Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c73f96525a | ||
|
|
510df192d8 | ||
|
|
2e7620f1eb | ||
|
|
93d4ee5241 | ||
|
|
1a7594be93 | ||
|
|
f464a7ee67 | ||
|
|
a1cd3d4609 | ||
|
|
358fb234a7 | ||
|
|
94fed58de3 | ||
|
|
49f452d9f2 | ||
|
|
40d07fe159 | ||
|
|
e150e6bede | ||
|
|
87a411f42d | ||
|
|
88f25b6743 | ||
|
|
bb0dc2e90f | ||
|
|
4a8939796e | ||
|
|
ba795ccd5c | ||
|
|
c9b5a677d3 | ||
|
|
affcb893fa | ||
|
|
e030807acb |
4
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
template: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
||||
14
.github/workflows/draft.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5.11.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
3
.gitignore
vendored
@@ -3,6 +3,9 @@ __pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# JavaScript packages
|
||||
node_modules/
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
|
||||
12
Dockerfile
@@ -3,20 +3,22 @@ FROM python:3-alpine
|
||||
# Getting things ready
|
||||
WORKDIR /usr/src/shynet
|
||||
COPY Pipfile.lock Pipfile ./
|
||||
COPY package.json package-lock.json ../
|
||||
# Django expects node_modules to be in its parent directory.
|
||||
|
||||
# Install dependencies & configure machine
|
||||
ARG GF_UID="500"
|
||||
ARG GF_GID="500"
|
||||
RUN apk update && \
|
||||
apk add gettext curl bash && \
|
||||
# URL from https://github.com/shlinkio/shlink/issues/596 :)
|
||||
curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp && \
|
||||
curl "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=G4Lm0C60yJsnkdPi&suffix=tar.gz" | tar -xvz -C /tmp && \
|
||||
apk add gettext curl bash npm && \
|
||||
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
|
||||
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
|
||||
mv /tmp/GeoLite2*/*.mmdb /etc && \
|
||||
apk del curl && \
|
||||
apk add --no-cache postgresql-libs && \
|
||||
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
|
||||
pip install pipenv && \
|
||||
npm i -P --prefix .. && \
|
||||
pip install pipenv~=2020.6.2 && \
|
||||
pipenv install --system --deploy && \
|
||||
apk --purge del .build-deps && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
|
||||
49
Pipfile
@@ -3,29 +3,30 @@ name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
|
||||
[packages]
|
||||
django = "*"
|
||||
django-allauth = "*"
|
||||
geoip2 = "*"
|
||||
whitenoise = "*"
|
||||
celery = "*"
|
||||
django-ipware = "*"
|
||||
pyyaml = "*"
|
||||
ua-parser = "*"
|
||||
user-agents = "*"
|
||||
emoji-country-flag = "*"
|
||||
rules = "*"
|
||||
gunicorn = "*"
|
||||
psycopg2-binary = "*"
|
||||
redis = "*"
|
||||
django-redis-cache = "*"
|
||||
pycountry = "*"
|
||||
ipaddress = "*"
|
||||
html2text = "*"
|
||||
django-health-check = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
[packages]
|
||||
django = "~=3.0"
|
||||
django-allauth = "~=0.42.0"
|
||||
geoip2 = "~=3.0.0"
|
||||
whitenoise = "~=5.1.0"
|
||||
celery = "~=4.4.6"
|
||||
django-ipware = "~=2.1.0"
|
||||
pyyaml = "~=5.3.1"
|
||||
ua-parser = "~=0.10.0"
|
||||
user-agents = "~=2.1"
|
||||
emoji-country-flag = "~=1.2.1"
|
||||
rules = "~=2.2"
|
||||
gunicorn = "~=20.0.4"
|
||||
psycopg2-binary = "~=2.8.5"
|
||||
redis = "~=3.5.3"
|
||||
django-redis-cache = "~=2.1.1"
|
||||
pycountry = "~=19.8.18"
|
||||
ipaddress = "~=1.0.23"
|
||||
html2text = "~=2020.1.16"
|
||||
django-health-check = "~=3.12.1"
|
||||
django-npm = "~=1.0.0"
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
|
||||
51
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "327b897f359bad486c08fc88fb70a1f9d2edaf1aadafcb1d31e5b3e144125ff7"
|
||||
"sha256": "ff989ac3413a6bd2253c9350c8f368a91942393221f1e47e5e39e60e457cc590"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -23,10 +23,10 @@
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
|
||||
"sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
|
||||
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
],
|
||||
"version": "==3.2.7"
|
||||
"version": "==3.2.10"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
@@ -37,18 +37,18 @@
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
"sha256:c3f4173f83ceb5a5c986c5fdaefb9456de3b0729a72a5776e46bd405fda7b647",
|
||||
"sha256:d1762d6065522879f341c3d67c2b9fe4615eb79756d59acb1434601d4aca474b"
|
||||
"sha256:ef17d7dffde7fc73ecab3a3b6389d93d3213bac53fa7f28e68e33647ad50b916",
|
||||
"sha256:fd77e4248bb1b7af5f7922dd8e81156f540306e3a5c4b1c24167c1f5f06025da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.4.5"
|
||||
"version": "==4.4.6"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5ad7e9a056d25ffa5082862e36f119f7f7cec6457fa07ee2f8c339814b80c9b1",
|
||||
"sha256:9cd41137dc19af6a5e03b630eefe7d1f458d964d406342dd3edf625839b944cc"
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
],
|
||||
"version": "==2020.4.5.2"
|
||||
"version": "==2020.6.20"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
@@ -66,11 +66,11 @@
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:db4c9b29615d17f808f2b1914d5cd73cd457c9fd90581195172c0888c210d944",
|
||||
"sha256:dd96f98ec1c3e60877d45cea7350215f16de409848d23cced8443db1b188bd9b"
|
||||
"sha256:045be31d68dfed684831e39ab1d9e77a595f1a393935cb43b6c5451d2e78c8a4",
|
||||
"sha256:ccf6c208424c0e1b0eaffd36efe12618a9ab4d0037e26f6ffceaa5277af985d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1a1"
|
||||
"version": "==3.1b1"
|
||||
},
|
||||
"django-allauth": {
|
||||
"hashes": [
|
||||
@@ -94,6 +94,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"django-npm": {
|
||||
"hashes": [
|
||||
"sha256:2e6bba65e728fa18b9db3c8dc0d4490b70cb7f43bacf60eb3654d7dcb6424272"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"django-redis-cache": {
|
||||
"hashes": [
|
||||
"sha256:06d4e48545243883f88dc9263dda6c8a0012cb7d0cee2d8758d8917eca92cece",
|
||||
@@ -142,10 +149,10 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
|
||||
"sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"version": "==2.9"
|
||||
"version": "==2.10"
|
||||
},
|
||||
"ipaddress": {
|
||||
"hashes": [
|
||||
@@ -157,10 +164,10 @@
|
||||
},
|
||||
"kombu": {
|
||||
"hashes": [
|
||||
"sha256:437b9cdea193cc2ed0b8044c85fd0f126bb3615ca2f4d4a35b39de7cacfa3c1a",
|
||||
"sha256:dc282bb277197d723bccda1a9ba30a27a28c9672d0ab93e9e51bb05a37bd29c3"
|
||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||
],
|
||||
"version": "==4.6.10"
|
||||
"version": "==4.6.11"
|
||||
},
|
||||
"maxminddb": {
|
||||
"hashes": [
|
||||
@@ -259,10 +266,10 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
|
||||
"sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
],
|
||||
"version": "==2.23.0"
|
||||
"version": "==2.24.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
|
||||
@@ -23,7 +23,7 @@ DJANGO_SECRET_KEY=random_string
|
||||
ALLOWED_HOSTS=*
|
||||
|
||||
# Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended)
|
||||
SIGNUPS_ENABLED=False
|
||||
ACCOUNT_SIGNUPS_ENABLED=False
|
||||
|
||||
# Should user email addresses be verified? Only set this to `required` if you've setup the email settings and allow
|
||||
# public sign-ups; otherwise, it's unnecessary.
|
||||
@@ -59,8 +59,8 @@ PORT=8080
|
||||
# Don't uncomment these unless you know what you are doing!
|
||||
# NUM_WORKERS=1
|
||||
# Make sure you set a REDIS_CACHE_LOCATION if you have more than one frontend worker/instance.
|
||||
# REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||
# REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
|
||||
# If CELERY_BROKER_URL is set, make sure CELERY_TASK_ALWAYS_EAGER is False and
|
||||
# that you have a separate queue consumer running somewhere via `celeryworker.sh`.
|
||||
# CELERY_TASK_ALWAYS_EAGER=False
|
||||
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||
|
||||
4
app.json
@@ -83,7 +83,7 @@
|
||||
"value": "*",
|
||||
"required": false
|
||||
},
|
||||
"SIGNUPS_ENABLED": {
|
||||
"ACCOUNT_SIGNUPS_ENABLED": {
|
||||
"description": "Set to True (capitalized) if you want people to be able to sign up for your Shynet instance (not recommended).",
|
||||
"value": "False",
|
||||
"required": false
|
||||
@@ -119,4 +119,4 @@
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 308 KiB After Width: | Height: | Size: 215 KiB |
BIN
images/logo.png
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 589 KiB After Width: | Height: | Size: 360 KiB |
BIN
images/slogo.png
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 931 B |
@@ -8,7 +8,7 @@ stringData:
|
||||
DEBUG: "False"
|
||||
ALLOWED_HOSTS: "*" # For better security, set this to your deployment's domain. Comma separated.
|
||||
DJANGO_SECRET_KEY: ""
|
||||
SIGNUPS_ENABLED: "False"
|
||||
ACCOUNT_SIGNUPS_ENABLED: "False"
|
||||
TIME_ZONE: "America/New_York"
|
||||
|
||||
# Redis configuration (if you use the default Kubernetes config, this will work)
|
||||
|
||||
3032
package-lock.json
generated
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "shynet",
|
||||
"description": "Modern, privacy-friendly, and cookie-free web analytics.",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/milesmcc/shynet.git"
|
||||
},
|
||||
"keywords": [
|
||||
"privacy",
|
||||
"analytics",
|
||||
"self-host"
|
||||
],
|
||||
"author": "R. Miles McCain <shynet@sendmiles.email>",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/milesmcc/shynet/issues"
|
||||
},
|
||||
"homepage": "https://github.com/milesmcc/shynet#readme",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.1",
|
||||
"a17t": "^0.1.4",
|
||||
"apexcharts": "^3.19.3",
|
||||
"inter-ui": "^3.13.1",
|
||||
"litepicker": "^1.5.7",
|
||||
"tailwindcss": "^1.4.6",
|
||||
"turbolinks": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css">
|
||||
<script async src="https://use.fontawesome.com/releases/v5.3.1/js/all.js"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
{% load static %}
|
||||
|
||||
<link rel="stylesheet" href="{% static 'a17t/dist/a17t.css' %}">
|
||||
<script async src="{% static '@fortawesome/fontawesome-free/js/all.min.js' %}" data-mutate-approach="sync"></script>
|
||||
<link href="{% static 'tailwindcss/dist/tailwind.min.css' %}" rel="stylesheet">
|
||||
<link href="{% static 'inter-ui/Inter (web)/inter.css' %}" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--family-primary: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
|
||||
@@ -92,4 +92,6 @@ def is_file(field):
|
||||
def add_class(field, css_class):
|
||||
if len(field.errors) > 0:
|
||||
css_class += " ~critical"
|
||||
if field.field.widget.attrs.get("class") != None:
|
||||
css_class += " " + field.field.widget.attrs["class"]
|
||||
return field.as_widget(attrs={"class": field.css_classes(extra_classes=css_class)})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
from hashlib import sha1
|
||||
from hashlib import sha256
|
||||
|
||||
import geoip2.database
|
||||
import user_agents
|
||||
@@ -73,7 +73,7 @@ def ingress_request(
|
||||
if payload.get("loadTime", 1) <= 0:
|
||||
payload["loadTime"] = None
|
||||
|
||||
association_id_hash = sha1()
|
||||
association_id_hash = sha256()
|
||||
association_id_hash.update(str(ip).encode("utf-8"))
|
||||
association_id_hash.update(str(user_agent).encode("utf-8"))
|
||||
session_cache_path = (
|
||||
@@ -121,11 +121,11 @@ def ingress_request(
|
||||
device=ua.device.family or ua.device.model or "",
|
||||
device_type=device_type,
|
||||
os=ua.os.family or "",
|
||||
asn=ip_data.get("asn", ""),
|
||||
country=ip_data.get("country", ""),
|
||||
asn=ip_data.get("asn") or "",
|
||||
country=ip_data.get("country") or "",
|
||||
longitude=ip_data.get("longitude"),
|
||||
latitude=ip_data.get("latitude"),
|
||||
time_zone=ip_data.get("time_zone", ""),
|
||||
time_zone=ip_data.get("time_zone") or "",
|
||||
)
|
||||
cache.set(
|
||||
session_cache_path, session.pk, timeout=settings.SESSION_MEMORY_TIMEOUT
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
window.onload = function () {
|
||||
var idempotency =
|
||||
Math.random().toString(36).substring(2, 15) +
|
||||
Math.random().toString(36).substring(2, 15);
|
||||
function sendUpdate() {
|
||||
// This is a lightweight and privacy-friendly analytics script from Shynet, a self-hosted
|
||||
// analytics tool. To give you full visibility into how your data is being monitored, this
|
||||
// file is intentionally not minified or obfuscated. To learn more about Shynet (and to view
|
||||
// its source code), visit <https://github.com/milesmcc/shynet>.
|
||||
//
|
||||
// This script only sends the current URL, the referrer URL, and the page load time. That's it!
|
||||
|
||||
var Shynet = {
|
||||
idempotency: null,
|
||||
heartbeatTaskId: null,
|
||||
sendHeartbeat: function () {
|
||||
try {
|
||||
if (document.hidden) {
|
||||
return;
|
||||
@@ -16,7 +22,7 @@ window.onload = function () {
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
idempotency: idempotency,
|
||||
idempotency: Shynet.idempotency,
|
||||
referrer: document.referrer,
|
||||
location: window.location.href,
|
||||
loadTime:
|
||||
@@ -24,8 +30,25 @@ window.onload = function () {
|
||||
window.performance.timing.navigationStart,
|
||||
})
|
||||
);
|
||||
} catch { }
|
||||
} catch (e) { }
|
||||
},
|
||||
newPageLoad: function () {
|
||||
if (Shynet.heartbeatTaskId != null) {
|
||||
clearInterval(Shynet.heartbeatTaskId);
|
||||
}
|
||||
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
||||
Shynet.sendHeartbeat();
|
||||
}
|
||||
setInterval(sendUpdate, parseInt("{{heartbeat_frequency}}"));
|
||||
sendUpdate();
|
||||
};
|
||||
|
||||
window.addEventListener("load", Shynet.newPageLoad);
|
||||
|
||||
{% if script_inject %}
|
||||
// The following is script is not part of Shynet, and was instead
|
||||
// provided by this site's administrator.
|
||||
//
|
||||
// -- START --
|
||||
{{script_inject|safe}}
|
||||
// -- END --
|
||||
{% endif %}
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
@@ -49,7 +50,15 @@ class ValidateServiceOriginsMixin:
|
||||
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
|
||||
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
resp["Access-Control-Allow-Origin"] = origins
|
||||
|
||||
if origins != "*":
|
||||
remote_origin = request.META.get("HTTP_ORIGIN")
|
||||
origins = [origin.strip() for origin in origins.split(",")]
|
||||
if remote_origin in origins:
|
||||
resp["Access-Control-Allow-Origin"] = remote_origin
|
||||
else:
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
|
||||
resp[
|
||||
"Access-Control-Allow-Headers"
|
||||
@@ -105,11 +114,14 @@ class ScriptView(ValidateServiceOriginsMixin, View):
|
||||
return render(
|
||||
self.request,
|
||||
"analytics/scripts/page.js",
|
||||
context={
|
||||
"endpoint": endpoint,
|
||||
"protocol": protocol,
|
||||
"heartbeat_frequency": heartbeat_frequency,
|
||||
},
|
||||
context=dict(
|
||||
{
|
||||
"endpoint": endpoint,
|
||||
"protocol": protocol,
|
||||
"heartbeat_frequency": heartbeat_frequency,
|
||||
"script_inject": self.get_script_inject(),
|
||||
}
|
||||
),
|
||||
content_type="application/javascript",
|
||||
)
|
||||
|
||||
@@ -125,3 +137,12 @@ class ScriptView(ValidateServiceOriginsMixin, View):
|
||||
return HttpResponse(
|
||||
json.dumps({"status": "OK"}), content_type="application/json"
|
||||
)
|
||||
|
||||
def get_script_inject(self):
|
||||
service_uuid = self.kwargs.get("service_uuid")
|
||||
script_inject = cache.get(f"script_inject_{service_uuid}")
|
||||
if script_inject == None:
|
||||
service = Service.objects.get(uuid=service_uuid)
|
||||
script_inject = service.script_inject
|
||||
cache.set(f"script_inject_{service_uuid}", script_inject, timeout=3600)
|
||||
return script_inject
|
||||
|
||||
@@ -6,13 +6,13 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_service_hide_referrer_regex'),
|
||||
("core", "0006_service_hide_referrer_regex"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='ignore_robots',
|
||||
model_name="service",
|
||||
name="ignore_robots",
|
||||
field=models.BooleanField(default=False),
|
||||
)
|
||||
]
|
||||
|
||||
23
shynet/core/migrations/0008_auto_20200628_1403.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1b1 on 2020-06-28 18:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_service_ignore_robots'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='script_inject',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
|
||||
),
|
||||
]
|
||||
@@ -73,6 +73,7 @@ class Service(models.Model):
|
||||
hide_referrer_regex = models.TextField(
|
||||
default="", blank=True, validators=[_validate_regex]
|
||||
)
|
||||
script_inject = models.TextField(default="", blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name", "uuid"]
|
||||
@@ -234,7 +235,9 @@ class Service(models.Model):
|
||||
"session_chart_data": json.dumps(
|
||||
[
|
||||
{"x": str(key), "y": value}
|
||||
for key, value in session_chart_data.items()
|
||||
for key, value in sorted(
|
||||
session_chart_data.items(), key=lambda k: k[0]
|
||||
)
|
||||
]
|
||||
),
|
||||
"online": True,
|
||||
|
||||
@@ -18,6 +18,7 @@ class ServiceForm(forms.ModelForm):
|
||||
"hide_referrer_regex",
|
||||
"origins",
|
||||
"collaborators",
|
||||
"script_inject"
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
@@ -27,6 +28,7 @@ class ServiceForm(forms.ModelForm):
|
||||
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"hide_referrer_regex": forms.TextInput(),
|
||||
"script_inject": forms.Textarea(attrs={'class':'font-mono', 'rows': 5})
|
||||
}
|
||||
labels = {
|
||||
"origins": "Allowed Hostnames",
|
||||
@@ -35,6 +37,7 @@ class ServiceForm(forms.ModelForm):
|
||||
"ignored_ips": "Ignored IP addresses",
|
||||
"ignore_robots": "Ignore robots",
|
||||
"hide_referrer_regex": "Hide specific referrers",
|
||||
"script_inject": "Additional injected JS",
|
||||
}
|
||||
help_texts = {
|
||||
"name": _("What should the service be called?"),
|
||||
@@ -47,10 +50,11 @@ class ServiceForm(forms.ModelForm):
|
||||
"ignored_ips": "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32').",
|
||||
"ignore_robots": "Should sessions generated by bots be excluded from tracking?",
|
||||
"hide_referrer_regex": "Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank.",
|
||||
"script_inject": "Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed.",
|
||||
}
|
||||
|
||||
collaborators = forms.CharField(
|
||||
help_text="Which users should have read-only access to this service? (Comma separated list of emails.)",
|
||||
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)",
|
||||
required=False,
|
||||
)
|
||||
|
||||
|
||||
0
shynet/dashboard/static/dashboard/js/base.js
Normal file
@@ -8,10 +8,10 @@
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% include 'a17t/head.html' %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/litepicker@1.2.0/dist/js/main.js"
|
||||
integrity="sha256-mOlCEHUNWZPYIrc5OFL4Ab2rsJGzIPld3cy1ok7Cfx0=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.18.1/dist/apexcharts.min.js"
|
||||
integrity="sha256-RalQXBZdisB04aaBsm+6YZ0b/iRYjX1MZn90m19AnCY=" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
||||
<script src="{% static 'litepicker/dist/js/main.js' %}"></script>
|
||||
<script src="{% static 'turbolinks/dist/turbolinks.js' %}"></script>
|
||||
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
{% if user.is_superuser %}
|
||||
{% url 'admin:index' as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Admin" url=url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label="Admin" disable_turbolinks=True url=url %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'account_email' as url %}
|
||||
|
||||
@@ -13,4 +13,5 @@
|
||||
{{form.ignore_robots|a17t}}
|
||||
{{form.hide_referrer_regex|a17t}}
|
||||
{{form.origins|a17t}}
|
||||
{{form.script_inject|a17t}}
|
||||
</details>
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
<div>
|
||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-gray-100{% endif %}"
|
||||
href="{{url}}">{{label}}</a>
|
||||
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{label}}</a>
|
||||
</div>
|
||||
@@ -84,6 +84,9 @@ class ServiceUpdateView(
|
||||
cache.set(
|
||||
f"service_origins_{self.object.uuid}", self.object.origins, timeout=3600
|
||||
)
|
||||
cache.set(
|
||||
f"script_inject_{self.object.uuid}", self.object.script_inject, timeout=3600
|
||||
)
|
||||
return resp
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
|
||||
@@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# import module sys to get the type of exception
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
@@ -17,7 +18,7 @@ import urllib.parse as urlparse
|
||||
from django.contrib.messages import constants as messages
|
||||
|
||||
# Increment on new releases
|
||||
VERSION = "v0.5.2"
|
||||
VERSION = "v0.6.0"
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -116,24 +117,26 @@ else:
|
||||
}
|
||||
|
||||
# Solution to removal of Heroku DB Injection
|
||||
if 'DATABASE_URL' in os.environ:
|
||||
if 'DATABASES' not in locals():
|
||||
if "DATABASE_URL" in os.environ:
|
||||
if "DATABASES" not in locals():
|
||||
DATABASES = {}
|
||||
url = urlparse.urlparse(os.environ['DATABASE_URL'])
|
||||
url = urlparse.urlparse(os.environ["DATABASE_URL"])
|
||||
|
||||
# Ensure default database exists.
|
||||
DATABASES['default'] = DATABASES.get('default', {})
|
||||
DATABASES["default"] = DATABASES.get("default", {})
|
||||
|
||||
# Update with environment configuration.
|
||||
DATABASES['default'].update({
|
||||
'NAME': url.path[1:],
|
||||
'USER': url.username,
|
||||
'PASSWORD': url.password,
|
||||
'HOST': url.hostname,
|
||||
'PORT': url.port,
|
||||
})
|
||||
if url.scheme == 'postgres':
|
||||
DATABASES['default']['ENGINE'] = 'django.db.backends.postgresql_psycopg2'
|
||||
DATABASES["default"].update(
|
||||
{
|
||||
"NAME": url.path[1:],
|
||||
"USER": url.username,
|
||||
"PASSWORD": url.password,
|
||||
"HOST": url.hostname,
|
||||
"PORT": url.port,
|
||||
}
|
||||
)
|
||||
if url.scheme == "postgres":
|
||||
DATABASES["default"]["ENGINE"] = "django.db.backends.postgresql_psycopg2"
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
|
||||
@@ -202,6 +205,11 @@ USE_TZ = True
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = "compiledstatic/"
|
||||
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
|
||||
STATICFILES_FINDERS = [
|
||||
"npm.finders.NpmFinder",
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
# Redis
|
||||
if not DEBUG and os.getenv("REDIS_CACHE_LOCATION") is not None:
|
||||
@@ -273,6 +281,21 @@ else:
|
||||
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
|
||||
EMAIL_USE_SSL = True
|
||||
|
||||
# NPM
|
||||
|
||||
NPM_ROOT_PATH = "../"
|
||||
|
||||
NPM_FILE_PATTERNS = {
|
||||
"a17t": ["dist/a17t.css"],
|
||||
"@fortawesome/fontawesome-free": ["js/all.min.js"],
|
||||
"tailwindcss": ["dist/tailwind.min.css"],
|
||||
"apexcharts": ["dist/apexcharts.min.js"],
|
||||
"litepicker": ["dist/js/main.js"],
|
||||
"turbolinks": ["dist/turbolinks.js"],
|
||||
"stimulus": ["dist/stimulus.umd.js"],
|
||||
"inter-ui": ["Inter (web)/*"],
|
||||
}
|
||||
|
||||
# Shynet
|
||||
|
||||
# Can everyone create services, or only superusers?
|
||||
|
||||
@@ -21,6 +21,6 @@ urlpatterns = [
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"),
|
||||
path("dashboard/", include(("dashboard.urls", "dashboard"), namespace="dashboard")),
|
||||
path("healthz/", include('health_check.urls')),
|
||||
path("healthz/", include("health_check.urls")),
|
||||
path("", include(("core.urls", "core"), namespace="core")),
|
||||
]
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript><img src="//localhost:8000/ingress/test_uuid/pixel.gif"></noscript>
|
||||
<script src="//localhost:8000/ingress/66015ce4-c69d-40fb-be8f-5535538d795e/script.js"></script>
|
||||
<noscript><img src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/pixel.gif"></noscript>
|
||||
<script src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||