Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Jason Carpenter 2020-04-24 15:10:56 -04:00
commit 23f1fdbb3f
13 changed files with 85 additions and 30 deletions

View File

@ -1,11 +1,4 @@
<p align="center"> # Getting Started
<h3 align="center">🔭 Shynet 🔭</h3>
<p align="center">
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
<br>
</p>
</p>
## Table of Contents ## Table of Contents
@ -50,6 +43,8 @@ DB_PORT=<your db port>
DJANGO_SECRET_KEY=<your Django secret key; just a random string> DJANGO_SECRET_KEY=<your Django secret key; just a random string>
# Don't leak error details to visitors, very important # Don't leak error details to visitors, very important
DEBUG=False DEBUG=False
# Unless you are using an external Celery task queue, make sure this
# is set to True.
CELERY_TASK_ALWAYS_EAGER=True CELERY_TASK_ALWAYS_EAGER=True
# For better security, set this to your deployment's domain. Comma separated. # For better security, set this to your deployment's domain. Comma separated.
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
@ -74,6 +69,14 @@ SERVER_EMAIL=Shynet <noreply@shynet.example.com>
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0 REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False # If set, make sure CELERY_TASK_ALWAYS_EAGER is False
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1 CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
# Other Shynet settings
# How frequently should the monitoring script "phone home" (in ms)?
SCRIPT_HEARTBEAT_FREQUENCY=5000
# Should only superusers (admins) be able to create services? This is helpful
# when you'd like to invite others to your Shynet instance but don't want
# them to be able to create services of their own.
ONLY_SUPERUSERS_CREATE=False
``` ```
4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`. 4. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest python manage.py migrate`.
@ -138,6 +141,8 @@ DB_PORT=<your db port>
DJANGO_SECRET_KEY=<your Django secret key; just a random string> DJANGO_SECRET_KEY=<your Django secret key; just a random string>
# Don't leak error details to visitors, very important # Don't leak error details to visitors, very important
DEBUG=False DEBUG=False
# Unless you are using an external Celery task queue, make sure this
# is set to True.
CELERY_TASK_ALWAYS_EAGER=True CELERY_TASK_ALWAYS_EAGER=True
# For better security, set this to your deployment's domain. Comma separated. # For better security, set this to your deployment's domain. Comma separated.
ALLOWED_HOSTS=* ALLOWED_HOSTS=*
@ -162,6 +167,15 @@ SERVER_EMAIL=Shynet <noreply@shynet.example.com>
REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0 REDIS_CACHE_LOCATION=redis://redis.default.svc.cluster.local/0
# If set, make sure CELERY_TASK_ALWAYS_EAGER is False # If set, make sure CELERY_TASK_ALWAYS_EAGER is False
CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1 CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
# Other Shynet settings
# How frequently should the monitoring script "phone home" (in ms)?
SCRIPT_HEARTBEAT_FREQUENCY=5000
# Should only superusers (admins) be able to create services? This is helpful
# when you'd like to invite others to your Shynet instance but don't want
# them to be able to create services of their own.
ONLY_SUPERUSERS_CREATE=False
``` ```
9. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest-ssl python manage.py migrate`. 9. Setup the Shynet database by running `docker run --env-file=<your env file> milesmcc/shynet:latest-ssl python manage.py migrate`.

View File

@ -1,14 +1,8 @@
<p align="center"> <img src="images/logo.png" height="50" alt="Shynet logo">
<h3 align="center">🔭 Shynet 🔭</h3>
<p align="center"> Modern, privacy-friendly, and cookie-free web analytics.
Web analytics that's self hosted, cookie free, privacy friendly, and useful(?)
<br> <br>
<br>
<a href="#installation"><strong>Getting started »</strong></a>
<br>
</p>
</p>
## Motivation ## Motivation
@ -85,7 +79,7 @@ Shynet is pretty simple, but there are a few key terms you need to know in order
## Installation ## Installation
You can find installation instructions in our [Getting Started Guide](GUIDE.md#installation)! You can find installation instructions in our [Getting Started Guide](GUIDE.md#installation).
## FAQ ## FAQ
@ -118,4 +112,4 @@ Shynet is made available under the [Apache License, version 2.0](LICENSE).
--- ---
a17t was created by [Miles McCain](https://miles.land) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land). Shynet was created by [Miles McCain](https://miles.land) ([@MilesMcCain](https://twitter.com/MilesMcCain)) at the [Recurse Center](https://recurse.com) using [a17t](https://a17t.miles.land).

BIN
images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,3 +1,10 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/a17t@0.1.3/dist/a17t.css"> <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> <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://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">
<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";
--family-secondary: var(--family-primary);
}
</style>

View File

@ -4,6 +4,9 @@ window.onload = function () {
Math.random().toString(36).substring(2, 15); Math.random().toString(36).substring(2, 15);
function sendUpdate() { function sendUpdate() {
try { try {
if (document.hidden) {
return;
}
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open( xhr.open(
"POST", "POST",
@ -21,8 +24,8 @@ window.onload = function () {
window.performance.timing.navigationStart, window.performance.timing.navigationStart,
}) })
); );
} catch {} } catch { }
} }
setInterval(sendUpdate, 5000); setInterval(sendUpdate, parseInt("{{heartbeat_frequency}}"));
sendUpdate(); sendUpdate();
}; };

View File

@ -8,7 +8,9 @@ from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
from django.core.cache import cache
from ipware import get_client_ip from ipware import get_client_ip
from core.models import Service
from ..tasks import ingress_request from ..tasks import ingress_request
@ -58,8 +60,15 @@ class PixelView(View):
@method_decorator(csrf_exempt, name="dispatch") @method_decorator(csrf_exempt, name="dispatch")
class ScriptView(View): class ScriptView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
service_uuid = self.kwargs.get("service_uuid")
origins = cache.get(f"service_origins_{service_uuid}")
if origins is None:
service = Service.objects.get(uuid=service_uuid)
origins = service.origins
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
resp = super().dispatch(request, *args, **kwargs) resp = super().dispatch(request, *args, **kwargs)
resp["Access-Control-Allow-Origin"] = "*" resp["Access-Control-Allow-Origin"] = origins
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST" resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[ resp[
"Access-Control-Allow-Headers" "Access-Control-Allow-Headers"
@ -82,10 +91,15 @@ class ScriptView(View):
}, },
) )
) )
heartbeat_frequency = settings.SCRIPT_HEARTBEAT_FREQUENCY
return render( return render(
self.request, self.request,
"analytics/scripts/page.js", "analytics/scripts/page.js",
context={"endpoint": endpoint, "protocol": protocol}, context={
"endpoint": endpoint,
"protocol": protocol,
"heartbeat_frequency": heartbeat_frequency,
},
content_type="application/javascript", content_type="application/javascript",
) )

View File

@ -8,7 +8,7 @@ from allauth.account.admin import EmailAddress
class ServiceForm(forms.ModelForm): class ServiceForm(forms.ModelForm):
class Meta: class Meta:
model = Service model = Service
fields = ["name", "link", "respect_dnt", "collaborators"] fields = ["name", "link", "respect_dnt", "origins", "collaborators"]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"origins": forms.TextInput(), "origins": forms.TextInput(),
@ -29,7 +29,6 @@ class ServiceForm(forms.ModelForm):
collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False) collaborators = forms.CharField(help_text="Which users should have read-only access to this service? (Comma separated list of emails.)", required=False)
def clean_collaborators(self): def clean_collaborators(self):
collaborators = [] collaborators = []
for collaborator_email in self.cleaned_data["collaborators"].split(","): for collaborator_email in self.cleaned_data["collaborators"].split(","):

View File

@ -0,0 +1,12 @@
{% load a17t_tags %}
{{form.name|a17t}}
{{form.link|a17t}}
{{form.collaborators|a17t}}
<details class="p-4 border rounded">
<summary class="cursor-pointer text-sm">Advanced settings</summary>
<hr class="sep h-4">
{{form.respect_dnt|a17t}}
{{form.origins|a17t}}
</details>

View File

@ -10,7 +10,7 @@
<form class="card ~neutral !low p-0 max-w-xl" method="POST"> <form class="card ~neutral !low p-0 max-w-xl" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
{{form|a17t}} {% include 'dashboard/includes/service_form.html' %}
</div> </div>
<div class="section ~urge !normal p-4"> <div class="section ~urge !normal p-4">
<button type="submit" class="button ~urge !high">Create</button> <button type="submit" class="button ~urge !high">Create</button>

View File

@ -23,7 +23,7 @@
<form class="card ~neutral !low p-0" method="POST"> <form class="card ~neutral !low p-0" method="POST">
{% csrf_token %} {% csrf_token %}
<div class="p-4"> <div class="p-4">
{{form|a17t}} {% include 'dashboard/includes/service_form.html' %}
</div> </div>
<div class="section ~neutral !normal p-4 flex justify-between"> <div class="section ~neutral !normal p-4 flex justify-between">
<div> <div>

View File

@ -12,6 +12,7 @@ from django.views.generic import (
UpdateView, UpdateView,
) )
from rules.contrib.views import PermissionRequiredMixin from rules.contrib.views import PermissionRequiredMixin
from django.core.cache import cache
from analytics.models import Session from analytics.models import Session
from core.models import Service from core.models import Service
@ -77,6 +78,13 @@ class ServiceUpdateView(
def get_success_url(self): def get_success_url(self):
return reverse("dashboard:service", kwargs={"pk": self.object.uuid}) return reverse("dashboard:service", kwargs={"pk": self.object.uuid})
def form_valid(self, *args, **kwargs):
resp = super().form_valid(*args, **kwargs)
cache.set(
f"service_origins_{self.object.uuid}", self.object.origins, timeout=3600
)
return resp
class ServiceDeleteView( class ServiceDeleteView(
LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView LoginRequiredMixin, PermissionRequiredMixin, SuccessMessageMixin, DeleteView

View File

@ -256,3 +256,7 @@ ONLY_SUPERUSERS_CREATE = os.getenv("ONLY_SUPERUSERS_CREATE", "True") == "True"
# Should the script use HTTPS to send the POST requests? The hostname is from # Should the script use HTTPS to send the POST requests? The hostname is from
# the django SITE default. (Edit it using the admin panel.) # the django SITE default. (Edit it using the admin panel.)
SCRIPT_USE_HTTPS = os.getenv("SCRIPT_USE_HTTPS", "True") == "True" SCRIPT_USE_HTTPS = os.getenv("SCRIPT_USE_HTTPS", "True") == "True"
# How frequently should the tracking script "phone home" with a heartbeat, in
# milliseconds?
SCRIPT_HEARTBEAT_FREQUENCY = int(os.getenv("SCRIPT_HEARTBEAT_FREQUENCY", "5000"))

View File

@ -7,8 +7,8 @@
</head> </head>
<body> <body>
<noscript><img src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/pixel.gif"></noscript> <noscript><img src="//localhost:8000/ingress/test_uuid/pixel.gif"></noscript>
<script src="//localhost:8000/ingress/211410e6-b401-4fe8-8740-7926368590be/script.js"></script> <script src="//localhost:8000/ingress/test_uuid/script.js"></script>
</body> </body>
</html> </html>