diff --git a/.gitignore b/.gitignore
index 91964e87..f0778887 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,8 @@ test
*.bat
server.ico
start.py
+icon.png
+logo.png
# Build
build
diff --git a/api/forms.py b/api/forms.py
index c3bda7d1..24c0463a 100644
--- a/api/forms.py
+++ b/api/forms.py
@@ -1,6 +1,73 @@
from django import forms
from api.models import UserProfile
+class GenerateForm(forms.Form):
+ #Platform
+ platform = forms.ChoiceField(choices=[('windows','Windows'),('linux','Linux (currently unavailable)'),('android','Android (testing now available)')], initial='windows')
+ version = forms.ChoiceField(choices=[('master','beta'),('1.3.2','1.3.2'),('1.3.1','1.3.1'),('1.3.0','1.3.0')], initial='1.3.2')
+ delayFix = forms.BooleanField(initial=True, required=False)
+
+ #General
+ exename = forms.CharField(label="Name for EXE file", required=True)
+ appname = forms.CharField(label="Custom App Name", required=False)
+ direction = forms.ChoiceField(widget=forms.RadioSelect, choices=[
+ ('incoming', 'Incoming Only'),
+ ('outgoing', 'Outgoing Only'),
+ ('both', 'Bidirectional')
+ ], initial='both')
+ installation = forms.ChoiceField(label="Disable Installation", choices=[
+ ('installationY', 'No, enable installation'),
+ ('installationN', 'Yes, DISABLE installation')
+ ], initial='installationY')
+ settings = forms.ChoiceField(label="Disable Settings", choices=[
+ ('settingsY', 'No, enable settings'),
+ ('settingsN', 'Yes, DISABLE settings')
+ ], initial='settingsY')
+
+ #Custom Server
+ serverIP = forms.CharField(label="Host", required=False)
+ apiServer = forms.CharField(label="API Server", required=False)
+ key = forms.CharField(label="Key", required=False)
+ urlLink = forms.CharField(label="Custom URL for links", required=False)
+
+ #Visual
+ iconfile = forms.FileField(label="Custom App Icon (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
+ logofile = forms.FileField(label="Custom App Logo (in .png format)", required=False, widget=forms.FileInput(attrs={'accept': 'image/png'}))
+ theme = forms.ChoiceField(choices=[
+ ('light', 'Light'),
+ ('dark', 'Dark'),
+ ('system', 'Follow System')
+ ], initial='system')
+ themeDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
+
+ #Security
+ passApproveMode = forms.ChoiceField(choices=[('password','Accept sessions via password'),('click','Accept sessions via click'),('password-click','Accepts sessions via both')],initial='password-click')
+ permanentPassword = forms.CharField(widget=forms.PasswordInput(), required=False)
+ runasadmin = forms.ChoiceField(choices=[('false','No'),('true','Yes')], initial='false')
+ denyLan = forms.BooleanField(initial=False, required=False)
+ enableDirectIP = forms.BooleanField(initial=False, required=False)
+ #ipWhitelist = forms.BooleanField(initial=False, required=False)
+ autoClose = forms.BooleanField(initial=False, required=False)
+
+ #Permissions
+ permissionsDorO = forms.ChoiceField(choices=[('default', 'Default'),('override', 'Override')], initial='default')
+ permissionsType = forms.ChoiceField(choices=[('custom', 'Custom'),('full', 'Full Access'),('view','Screen share')], initial='custom')
+ enableKeyboard = forms.BooleanField(initial=True, required=False)
+ enableClipboard = forms.BooleanField(initial=True, required=False)
+ enableFileTransfer = forms.BooleanField(initial=True, required=False)
+ enableAudio = forms.BooleanField(initial=True, required=False)
+ enableTCP = forms.BooleanField(initial=True, required=False)
+ enableRemoteRestart = forms.BooleanField(initial=True, required=False)
+ enableRecording = forms.BooleanField(initial=True, required=False)
+ enableBlockingInput = forms.BooleanField(initial=True, required=False)
+ enableRemoteModi = forms.BooleanField(initial=False, required=False)
+
+ #Other
+ removeWallpaper = forms.BooleanField(initial=True, required=False)
+
+ defaultManual = forms.CharField(widget=forms.Textarea, required=False)
+ overrideManual = forms.CharField(widget=forms.Textarea, required=False)
+
class AddPeerForm(forms.Form):
clientID = forms.CharField(label="Client Rustdesk ID", required=True)
alias = forms.CharField(label="Client alias", required=True)
diff --git a/api/migrations/0004_githubrun.py b/api/migrations/0004_githubrun.py
new file mode 100644
index 00000000..68a4fff5
--- /dev/null
+++ b/api/migrations/0004_githubrun.py
@@ -0,0 +1,21 @@
+# Generated by Django 5.0.3 on 2024-11-12 19:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0003_rustdesdevice_ip_rustdeskpeer_ip'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GithubRun',
+ fields=[
+ ('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.CharField(max_length=100, verbose_name='uuid')),
+ ('status', models.CharField(max_length=100, verbose_name='status')),
+ ],
+ ),
+ ]
diff --git a/api/models_work.py b/api/models_work.py
index 6813807e..c95e4143 100644
--- a/api/models_work.py
+++ b/api/models_work.py
@@ -144,3 +144,8 @@ class ShareLinkAdmin(admin.ModelAdmin):
list_display = ('shash', 'uid', 'peers', 'is_used', 'is_expired', 'create_time')
search_fields = ('peers', )
list_filter = ('is_used', 'uid', 'is_expired' )
+
+class GithubRun(models.Model):
+ id = models.IntegerField(verbose_name="ID",primary_key=True)
+ uuid = models.CharField(verbose_name="uuid", max_length=100)
+ status = models.CharField(verbose_name="status", max_length=100)
diff --git a/api/templates/base.html b/api/templates/base.html
index 8285c39f..fa4001d3 100644
--- a/api/templates/base.html
+++ b/api/templates/base.html
@@ -65,7 +65,7 @@
Home
Share
Installers
-
+ Client Generator
{% if u.is_admin %}
Admin Panel
diff --git a/api/templates/generator.html b/api/templates/generator.html
new file mode 100644
index 00000000..fa1d7b88
--- /dev/null
+++ b/api/templates/generator.html
@@ -0,0 +1,248 @@
+{% extends "base.html" %}
+{% block title %}RustDesk WebUI{% endblock %}
+{% block legend_name %}Client Generator{% endblock %}
+{% block content %}
+
+
+
+
+ RustDesk Custom Client Builder
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/api/templates/installers.html b/api/templates/installers.html
index c2ce1ed8..e9910180 100644
--- a/api/templates/installers.html
+++ b/api/templates/installers.html
@@ -71,6 +71,29 @@
+
+
+
+
+
+ Client Generator
+
+
+
+ | File |
+ Date |
+
+ {% for filename, fileinfo in client_files.items %}
+
+ | {{filename}} |
+ {{fileinfo.modified}} |
+
+ {% endfor %}
+
+
+
+
+
{% endblock %}
diff --git a/api/templates/waiting.html b/api/templates/waiting.html
new file mode 100644
index 00000000..97597618
--- /dev/null
+++ b/api/templates/waiting.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% block title %}RustDesk WebUI{% endblock %}
+{% block legend_name %}Client Generator{% endblock %}
+{% block content %}
+Please wait...This can take 20-30 minutes (or longer if there are other users).
+You will be redirected to the Installers page to download your custom installer once generator is complete.
+You can close this window at anytime, or stay here to get status updates.
+
Status: {{status}}
+
+{% endblock %}
\ No newline at end of file
diff --git a/api/urls.py b/api/urls.py
index 0285b3ed..9aefc6be 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -27,4 +27,10 @@
url(r'^delete_peer',views.delete_peer),
url(r'^edit_peer',views.edit_peer),
url(r'^assign_peer',views.assign_peer),
+ url(r'^generator',views.generator_view),
+ url(r'^check_for_file',views.check_for_file),
+ url(r'^download_client',views.download_client),
+ url(r'^creategh',views.create_github_run),
+ url(r'^updategh',views.update_github_run),
+ url(r'^save_custom_client',views.save_custom_client),
]
diff --git a/api/views.py b/api/views.py
index 30a38cbb..ca4e7f1c 100644
--- a/api/views.py
+++ b/api/views.py
@@ -22,3 +22,4 @@
from .views_front import *
from .views_api import *
+from .views_generator import *
diff --git a/api/views_front.py b/api/views_front.py
index 9d3b1193..fd820af0 100644
--- a/api/views_front.py
+++ b/api/views_front.py
@@ -1,4 +1,5 @@
# cython:language_level=3
+import os
from django.shortcuts import render
from django.http import HttpResponseRedirect
from django.contrib.auth.hashers import make_password
@@ -348,7 +349,18 @@ def share(request):
@login_required(login_url='/api/user_action?action=login')
def installers(request):
- return render(request, 'installers.html')
+ custom = os.path.join('clients','custom')
+ client_custom_files = {}
+ if os.path.exists(custom):
+ for file in os.listdir(custom):
+ filepath = os.path.join(custom,file)
+ modified = datetime.datetime.fromtimestamp(os.path.getmtime(filepath)).strftime('%Y-%m-%d %I:%M:%S %p')
+ client_custom_files[file] = {
+ 'file': file,
+ 'modified': modified,
+ 'path': custom
+ }
+ return render(request, 'installers.html', {'client_custom_files': client_custom_files})
def get_conn_log():
logs = ConnLog.objects.all()
diff --git a/api/views_generator.py b/api/views_generator.py
new file mode 100644
index 00000000..c8bc2e31
--- /dev/null
+++ b/api/views_generator.py
@@ -0,0 +1,291 @@
+import io
+from pathlib import Path
+from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
+from django.shortcuts import render
+from django.contrib.auth.decorators import login_required
+from django.core.files.base import ContentFile
+import os
+import re
+import requests
+import base64
+import json
+import uuid
+import pathlib
+from django.conf import settings as _settings
+from django.db.models import Q
+from .forms import GenerateForm
+from .models import GithubRun
+from PIL import Image
+from urllib.parse import quote
+
+@login_required(login_url='/api/user_action?action=login')
+def generator_view(request):
+ if request.method == 'POST':
+ form = GenerateForm(request.POST, request.FILES)
+ if form.is_valid():
+ platform = form.cleaned_data['platform']
+ version = form.cleaned_data['version']
+ delayFix = form.cleaned_data['delayFix']
+ server = form.cleaned_data['serverIP']
+ key = form.cleaned_data['key']
+ apiServer = form.cleaned_data['apiServer']
+ urlLink = form.cleaned_data['urlLink']
+ if not server:
+ server = 'rs-ny.rustdesk.com' #default rustdesk server
+ if not key:
+ key = 'OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=' #default rustdesk key
+ if not apiServer:
+ apiServer = server+":21114"
+ if not urlLink:
+ urlLink = "https://rustdesk.com"
+ direction = form.cleaned_data['direction']
+ installation = form.cleaned_data['installation']
+ settings = form.cleaned_data['settings']
+ appname = form.cleaned_data['appname']
+ filename = form.cleaned_data['exename']
+ permPass = form.cleaned_data['permanentPassword']
+ theme = form.cleaned_data['theme']
+ themeDorO = form.cleaned_data['themeDorO']
+ runasadmin = form.cleaned_data['runasadmin']
+ passApproveMode = form.cleaned_data['passApproveMode']
+ denyLan = form.cleaned_data['denyLan']
+ enableDirectIP = form.cleaned_data['enableDirectIP']
+ #ipWhitelist = form.cleaned_data['ipWhitelist']
+ autoClose = form.cleaned_data['autoClose']
+ permissionsDorO = form.cleaned_data['permissionsDorO']
+ permissionsType = form.cleaned_data['permissionsType']
+ enableKeyboard = form.cleaned_data['enableKeyboard']
+ enableClipboard = form.cleaned_data['enableClipboard']
+ enableFileTransfer = form.cleaned_data['enableFileTransfer']
+ enableAudio = form.cleaned_data['enableAudio']
+ enableTCP = form.cleaned_data['enableTCP']
+ enableRemoteRestart = form.cleaned_data['enableRemoteRestart']
+ enableRecording = form.cleaned_data['enableRecording']
+ enableBlockingInput = form.cleaned_data['enableBlockingInput']
+ enableRemoteModi = form.cleaned_data['enableRemoteModi']
+ removeWallpaper = form.cleaned_data['removeWallpaper']
+ defaultManual = form.cleaned_data['defaultManual']
+ overrideManual = form.cleaned_data['overrideManual']
+
+
+ filename = re.sub(r'[^\w\s-]', '_', filename).strip()
+ myuuid = str(uuid.uuid4())
+ protocol = 'https' if request.is_secure() else 'http'
+ host = request.get_host()
+ full_url = f"{protocol}://{host}/api"
+ try:
+ iconfile = form.cleaned_data['iconfile']
+ #iconbase64 = resize_and_encode_icon(iconfile)
+ iconlink = save_png(iconfile,myuuid,full_url)
+ except:
+ print("failed to get icon, using default")
+ #iconbase64 = b"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"
+ iconlink = "false"
+ try:
+ logofile = form.cleaned_data['logofile']
+ #logobase64 = resize_and_encode_icon(logofile)
+ logolink = save_png(logofile,myuuid,full_url)
+ except:
+ print("failed to get logo")
+ #logobase64 = b""
+ logolink = "false"
+
+ ###create the custom.txt json here and send in as inputs below
+ decodedCustom = {}
+ if direction != "Both":
+ decodedCustom['conn-type'] = direction
+ if installation == "installationN":
+ decodedCustom['disable-installation'] = 'Y'
+ if settings == "settingsN":
+ decodedCustom['disable-settings'] = 'Y'
+ if appname.upper != "rustdesk".upper and appname != "":
+ decodedCustom['app-name'] = appname
+ decodedCustom['override-settings'] = {}
+ decodedCustom['default-settings'] = {}
+ if permPass != "":
+ decodedCustom['password'] = permPass
+ if theme != "system":
+ if themeDorO == "default":
+ decodedCustom['default-settings']['theme'] = theme
+ elif themeDorO == "override":
+ decodedCustom['override-settings']['theme'] = theme
+ decodedCustom['approve-mode'] = passApproveMode
+ decodedCustom['enable-lan-discovery'] = 'N' if denyLan else 'Y'
+ decodedCustom['direct-server'] = 'Y' if enableDirectIP else 'N'
+ decodedCustom['allow-auto-disconnect'] = 'Y' if autoClose else 'N'
+ decodedCustom['allow-remove-wallpaper'] = 'Y' if removeWallpaper else 'N'
+ if permissionsDorO == "default":
+ decodedCustom['default-settings']['access-mode'] = permissionsType
+ decodedCustom['default-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N'
+ decodedCustom['default-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N'
+ decodedCustom['default-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N'
+ decodedCustom['default-settings']['enable-audio'] = 'Y' if enableAudio else 'N'
+ decodedCustom['default-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N'
+ decodedCustom['default-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N'
+ decodedCustom['default-settings']['enable-record-session'] = 'Y' if enableRecording else 'N'
+ decodedCustom['default-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N'
+ decodedCustom['default-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N'
+ else:
+ decodedCustom['override-settings']['access-mode'] = permissionsType
+ decodedCustom['override-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N'
+ decodedCustom['override-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N'
+ decodedCustom['override-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N'
+ decodedCustom['override-settings']['enable-audio'] = 'Y' if enableAudio else 'N'
+ decodedCustom['override-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N'
+ decodedCustom['override-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N'
+ decodedCustom['override-settings']['enable-record-session'] = 'Y' if enableRecording else 'N'
+ decodedCustom['override-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N'
+ decodedCustom['override-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N'
+
+ for line in defaultManual.splitlines():
+ k, value = line.split('=')
+ decodedCustom['default-settings'][k.strip()] = value.strip()
+
+ for line in overrideManual.splitlines():
+ k, value = line.split('=')
+ decodedCustom['override-settings'][k.strip()] = value.strip()
+
+ decodedCustomJson = json.dumps(decodedCustom)
+
+ string_bytes = decodedCustomJson.encode("ascii")
+ base64_bytes = base64.b64encode(string_bytes)
+ encodedCustom = base64_bytes.decode("ascii")
+
+ #github limits inputs to 10, so lump extras into one with json
+ extras = {}
+ extras['runasadmin'] = runasadmin
+ extras['urlLink'] = urlLink
+ extras['delayFix'] = 'true' if delayFix else 'false'
+ extras['version'] = version
+ extras['rdgen'] = 'false'
+ extra_input = json.dumps(extras)
+
+ if _settings.GHUSER == '':
+ ####run the github actions through rdgen.crayoneater.org
+ url = 'https://rdgen.crayoneater.org/startgh'
+ data = {
+ "server":server,
+ "key":key,
+ "apiServer":apiServer,
+ "custom":encodedCustom,
+ "uuid":myuuid,
+ "iconlink":iconlink,
+ "logolink":logolink,
+ "appname":appname,
+ "extras":extra_input,
+ "filename":filename,
+ "platform":platform
+ }
+ response = requests.post(url, json=data)
+ else:
+ ####run the github actions through user's own github fork of rdgen
+ url = 'https://api.github.com/repos/'+_settings.GHUSER+'/rdgen/actions/workflows/generator-'+platform+'.yml/dispatches'
+ data = {
+ "ref":"master",
+ "inputs":{
+ "server":server,
+ "key":key,
+ "apiServer":apiServer,
+ "custom":encodedCustom,
+ "uuid":myuuid,
+ "iconlink":iconlink,
+ "logolink":logolink,
+ "appname":appname,
+ "extras":extra_input,
+ "filename":filename
+ }
+ }
+ headers = {
+ 'Accept': 'application/vnd.github+json',
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer '+_settings.GHBEARER,
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ response = requests.post(url, json=data, headers=headers)
+
+ print(response)
+ if response.status_code == 204 or response.status_code == 200:
+ create_github_run(myuuid)
+ return render(request, 'waiting.html', {'filename':filename, 'uuid':myuuid, 'status':"Starting generator...please wait", 'platform':platform})
+ else:
+ return JsonResponse({"error": "Something went wrong"})
+ else:
+ form = GenerateForm()
+ return render(request, 'generator.html', {'form': form})
+
+@login_required(login_url='/api/user_action?action=login')
+def check_for_file(request):
+ filename = request.GET['filename']
+ uuid = request.GET['uuid']
+ platform = request.GET['platform']
+ gh_run = GithubRun.objects.filter(Q(uuid=uuid)).first()
+ status = gh_run.status
+
+ if status == "Success":
+ return HttpResponseRedirect('/api/installers')
+ else:
+ return render(request, 'waiting.html', {'filename':filename, 'uuid':uuid, 'status':status, 'platform':platform})
+
+@login_required(login_url='/api/user_action?action=login')
+def download_client(request):
+ filename = request.GET['filename']
+ uuid = request.GET['uuid']
+ file_path = os.path.join('exe',uuid,filename)
+ with open(file_path, 'rb') as file:
+ response = HttpResponse(file, headers={
+ 'Content-Type': 'application/vnd.microsoft.portable-executable',
+ 'Content-Disposition': f'attachment; filename="{filename}"'
+ })
+
+ return response
+
+def save_png(file, uuid, domain):
+ file_save_path = "png/%s/%s" % (uuid, quote(file.name))
+ Path("png/%s" % uuid).mkdir(parents=True, exist_ok=True)
+ with open(file_save_path, "wb+") as f:
+ for chunk in file.chunks():
+ f.write(chunk)
+ imageJson = {}
+ imageJson['url'] = domain
+ imageJson['uuid'] = uuid
+ imageJson['file'] = quote(file.name)
+ #return "%s/%s" % (domain, file_save_path)
+ return json.dumps(imageJson)
+
+def get_png(request):
+ filename = request.GET['filename']
+ uuid = request.GET['uuid']
+ #filename = filename+".exe"
+ file_path = os.path.join('png',uuid,filename)
+ with open(file_path, 'rb') as file:
+ response = HttpResponse(file, headers={
+ 'Content-Type': 'application/vnd.microsoft.portable-executable',
+ 'Content-Disposition': f'attachment; filename="{filename}"'
+ })
+
+ return response
+
+
+def create_github_run(myuuid):
+ new_github_run = GithubRun(
+ uuid=myuuid,
+ status="Starting generator...please wait"
+ )
+ new_github_run.save()
+
+def update_github_run(request):
+ data = json.loads(request.body)
+ myuuid = data.get('uuid')
+ mystatus = data.get('status')
+ GithubRun.objects.filter(Q(uuid=myuuid)).update(status=mystatus)
+ return HttpResponse('')
+
+def save_custom_client(request):
+ file = request.FILES['file']
+ file_save_path = "clients/custom/%s" % file.name
+ pathlib.Path("clients/custom").mkdir(parents=True, exist_ok=True)
+ with open(file_save_path, "wb+") as f:
+ for chunk in file.chunks():
+ f.write(chunk)
+
+ return HttpResponse("File saved successfully!")
\ No newline at end of file
diff --git a/generator_setup.md b/generator_setup.md
new file mode 100644
index 00000000..f8bcdb19
--- /dev/null
+++ b/generator_setup.md
@@ -0,0 +1,35 @@
+## By default, the generator will use rdgen.crayoneater.org to generate your custom client installers. Follow these instructions to use your own github account to generate your custom client installers.
+
+## To fully host the client generator yourself, you will need to following:
+
+
+ - A Github account with a fork of the rdgen rdgen
+ - A Github fine-grained access token with permissions for your rdgen repository
+
+ - login to your github account
+ - click on your profile picture at the top right, click Settings
+ - at the bottom of the left panel, click Developer Settings
+ - click Personal access tokens
+ - click Fine-grained tokens
+ - click Generate new token
+ - give a token name, change expiration to whatever you want
+ - under Repository acces, select Only select repositories, then pick your rdgen repo
+ - give Read and Write access to actions and workflows
+
+
+ - Setup environment variables / secrets:
+
+ - environment variables on the server running rdgen:
+
+ - GHUSER="your github username"
+ - GHBEARER="your fine-graned access token"
+
+ - optional github secrets (setup on your github account for your rdgen repo):
+
+ - WINDOWS_PFX_BASE64
+ - WINDOWS_PFX_PASSWORD
+ - WINDOWS_PFX_SHA1_THUMBPRINT
+
+
+
+
\ No newline at end of file
diff --git a/rustdesk_server_api/settings.py b/rustdesk_server_api/settings.py
index ec1d5cbb..5292857b 100644
--- a/rustdesk_server_api/settings.py
+++ b/rustdesk_server_api/settings.py
@@ -30,6 +30,9 @@
ALLOWED_HOSTS = ["*"]
AUTH_USER_MODEL = 'api.UserProfile' #AppName.自定义user
+GHUSER = os.environ.get("GHUSER", '')
+GHBEARER = os.environ.get("GHBEARER", '')
+
# Application definition
INSTALLED_APPS = [