Merge branch 'master' int trust - so it doesn't look stale

This commit is contained in:
Oliver Falk
2019-03-01 15:50:59 +01:00
13 changed files with 157 additions and 250 deletions

View File

@@ -1,12 +1,9 @@
image: centos:centos7
image: ofalk/centos7-python36
before_script:
- yum install -y -t https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
- yum -y -t install python36 python34-pip python36-devel unzip mysql-devel gcc git openldap-devel
- pip3 install pip --upgrade
- pip install virtualenv --upgrade
- virtualenv -p python3.6 /tmp/.virtualenv
- source /tmp/.virtualenv/bin/activate
- pip install Pillow
- pip install -r requirements.txt
- pip install python-coveralls
- pip install coverage

View File

@@ -52,7 +52,7 @@ OPENID_CREATE_USERS = True
OPENID_UPDATE_DETAILS_FROM_SREG = True
SITE_NAME = os.environ.get('SITE_NAME', 'libravatar')
IVATAR_VERSION = '1.0'
IVATAR_VERSION = '1.1'
SECURE_BASE_URL = os.environ.get('SECURE_BASE_URL', 'https://avatars.linux-kernel.at/avatar/')
BASE_URL = os.environ.get('BASE_URL', 'http://avatars.linux-kernel.at/avatar/')
@@ -102,11 +102,14 @@ else:
if 'test' in sys.argv or 'collectstatic' in sys.argv:
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
else:
try:
ANYMAIL = { # pragma: no cover
'MAILGUN_API_KEY': os.environ['IVATAR_MAILGUN_API_KEY'],
'MAILGUN_SENDER_DOMAIN': os.environ['IVATAR_MAILGUN_SENDER_DOMAIN'],
}
EMAIL_BACKEND = 'anymail.backends.mailgun.EmailBackend' # pragma: no cover
except Exception as exc:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
SERVER_EMAIL = os.environ.get('SERVER_EMAIL', 'ivatar@mg.linux-kernel.at')
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'ivatar@mg.linux-kernel.at')

View File

@@ -1,139 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Oliver Falk <oliver@linux-kernel.at>
#
# This file is part of Libravatar
#
# Libravatar is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Libravatar is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Libravatar. If not, see <http://www.gnu.org/licenses/>.
import base64
import gzip
import json
import os
import sys
from xml.sax import saxutils
import hashlib
# pylint: disable=relative-import
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libravatar.settings")
import libravatar.settings as settings
from libravatar.utils import create_logger, is_hex
import django
django.setup()
from django.contrib.auth.models import User
os.umask(022)
LOGGER = create_logger('exportaccount')
SCHEMA_ROOT = 'https://www.libravatar.org/schemas/export/0.2'
SCHEMA_XSD = '%s/export.xsd' % SCHEMA_ROOT
def xml_header():
return '''<?xml version="1.0" encoding="UTF-8"?>
<user xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="%s %s"
xmlns="%s">\n''' % (SCHEMA_ROOT, SCHEMA_XSD, SCHEMA_ROOT)
def xml_footer():
return '</user>\n'
def xml_account(username, password):
escaped_username = saxutils.quoteattr(username)
escaped_site_url = saxutils.quoteattr(settings.SITE_URL)
escaped_password = saxutils.quoteattr(password)
return ' <account username=%s password=%s site=%s/>\n' % (escaped_username, escaped_password, escaped_site_url)
def xml_email(emails):
returnstring = " <emails>\n"
for email in emails:
returnstring += ' <email photo_id="' + str(email.photo_id) + '">' + email.email.encode('utf-8') + '</email>' + "\n"
returnstring += " </emails>\n"
return returnstring
def xml_openid(openids):
returnstring = " <openids>\n"
for openid in openids:
returnstring += ' <openid photo_id="' + str(openid.photo_id) + '">' + openid.openid.encode('utf-8') + '</openid>' + "\n"
returnstring += " </openids>\n"
return returnstring
def xml_photos(photos):
s = ' <photos>\n'
for photo in photos:
(photo_filename, photo_format, id) = photo
encoded_photo = encode_photo(photo_filename, photo_format)
if encoded_photo:
s += ''' <photo id="%s" encoding="base64" format=%s>
%s
</photo>\n''' % (id, saxutils.quoteattr(photo_format), encoded_photo)
s += ' </photos>\n'
return s
def encode_photo(photo_filename, photo_format):
filename = settings.USER_FILES_ROOT + photo_filename + '.' + photo_format
if not os.path.isfile(filename):
LOGGER.warning('Photo not found: %s', filename)
return None
photo_content = None
with open(filename) as photo:
photo_content = photo.read()
if not photo_content:
LOGGER.warning('Could not read photo: %s', filename)
return None
return base64.b64encode(photo_content)
def main(argv=None):
if argv is None:
argv = sys.argv
if(len(sys.argv) > 1):
userobjs = User.objects.filter(username=sys.argv[1])
else:
userobjs = User.objects.all()
for user in userobjs:
hash_object = hashlib.new('sha256')
hash_object.update(user.username + user.password)
file_hash = hash_object.hexdigest()
photos = []
for photo in user.photos.all():
photo_details = (photo.filename, photo.format, photo.id)
photos.append(photo_details)
username = user.username
dest_filename = settings.EXPORT_FILES_ROOT + file_hash + '.xml.gz'
destination = gzip.open(dest_filename, 'w')
destination.write(xml_header())
destination.write(xml_account(username, user.password))
destination.write(xml_email(user.confirmed_emails.all()))
destination.write(xml_openid(user.confirmed_openids.all()))
destination.write(xml_photos(photos))
destination.write(xml_footer())
destination.close()
print(dest_filename)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -37,10 +37,10 @@ from .gravatar import get_photo as get_gravatar_photo
def file_format(image_type):
'''
Helper method returning a 3 character long image type
Helper method returning a short image type
'''
if image_type == 'JPEG':
return 'jpg'
return 'jpeg'
elif image_type == 'PNG':
return 'png'
elif image_type == 'GIF':
@@ -52,7 +52,7 @@ def pil_format(image_type):
'''
Helper method returning the 'encoder name' for PIL
'''
if image_type == 'jpg':
if image_type == 'jpg' or image_type == 'jpeg':
return 'JPEG'
elif image_type == 'png':
return 'PNG'

View File

@@ -358,7 +358,7 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
# Probably not the best way to access the content type
self.assertEqual(
response['Content-Type'],
'image/jpg',
'image/jpeg',
'Content type wrong!?')
self.assertEqual(
@@ -638,10 +638,10 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(
str(list(response.context[0]['messages'])[0]),
'Successfully uploaded',
'JPG upload failed?!')
'JPEG upload failed?!')
self.assertEqual(
self.user.photo_set.first().format, 'jpg',
'Format must be jpg, since we uploaded a jpg!')
self.user.photo_set.first().format, 'jpeg',
'Format must be jpeg, since we uploaded a jpeg!')
self.test_confirm_email()
self.user.confirmedemail_set.first().photo = self.user.photo_set.first()
urlobj = urlsplit(

View File

@@ -914,6 +914,9 @@ class PasswordResetView(PasswordResetViewOriginal):
try:
confirmed_email = ConfirmedEmail.objects.get(email=request.POST['email'])
confirmed_email.user.email = confirmed_email.email
if not confirmed_email.user.password:
random_pass = User.objects.make_random_password()
confirmed_email.user.set_pasword(random_pass)
confirmed_email.user.save()
except Exception as exc:
pass

View File

@@ -56,8 +56,10 @@ class CheckForm(forms.Form):
required=True,
)
default_url = forms.URLField(
default_url = forms.CharField(
label=_('Default URL'),
min_length=1,
max_length=MAX_LENGTH_URL,
required=False,
)

View File

@@ -8,6 +8,12 @@
<h1>{% trans 'Check e-mail or openid' %}</h1>
{% if form.errors %}
<p class="error">{% trans "Please correct errors below:" %}<br>
{{ form.errors|join:', ' }}
</p>
{% endif %}
<div style="max-width:640px">
<form method="post" name="check">
{% csrf_token %}
@@ -17,8 +23,8 @@
<input type="text" name="openid" maxlength="255" minlength="11" class="form-control" placeholder="{% trans 'OpenID' %}" {% if openidurl %} value="{{ form.openid.value }}" {% endif %} id="id_openid"></div>
<div class="form-group"><label for="id_size">{% trans 'Size' %}</label>
<input type="number" name="size" min="5" max="512" class="form-control" placeholder="{% trans 'Size' %}" {% if mailurl or openidurl %} value="{{ form.size.value }}" {% else %} value="100" {% endif %} required id="id_size"></div>
<div class="form-group"><label for="id_default_url">{% trans 'Default URL' %}</label>
<input type="url" name="default_url" class="form-control" placeholder="{% trans 'Default URL' %}" {% if mailurl or openidurl %} value="{{ form.default_url.value }}" {% endif %} id="id_default_url"></div>
<div class="form-group"><label for="id_default_url">{% trans 'Default URL or special keyword' %}</label>
<input type="text" name="default_url" class="form-control" placeholder="{% trans 'Default' %}" {% if mailurl or openidurl %} value="{{ form.default_url.value }}" {% endif %} id="id_default_url"></div>
<div class="form-group">
<button type="submit" class="btn btn-default">{% trans 'Check' %}</button>
</div>
@@ -56,8 +62,8 @@
<h3 class="panel-title">SHA256 <i class="fa fa-lock" title="Secure connection (https)"></i>&nbsp;<i class="fa fa-at" title="mail: {{ form.mail.value }}"></i></h3>
</div>
<div class="panel-body">
<a href="{{ SECURE_BASE_URL }}{{ mail_hash256 }}?s={{ size }}">
<center><img src="{{ SECURE_BASE_URL }}{{ mail_hash256 }}?s={{ size }}" style="max-width: {{ size }}px; max-height: {{ size }}px;"></center>
<a href="{{ mailurl_secure_256 }}">
<center><img src="{{ mailurl_secure_256 }}" style="max-width: {{ size }}px; max-height: {{ size }}px;"></center>
</a>
</div>
</div>

View File

@@ -37,44 +37,51 @@
<!-- TODO TODO TODO: I need better styling -->
{% if result %}
<hr/>
<table>
<tr>
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTP avatar server:' %}</td>
<td>
<h2>The following servers will be used for your domain</h2>
<div class="panel panel-tortin" style="width:intrinsic;margin-left:30px;float:left">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-unlock-alt"></i>&nbsp;HTTP Server</h3>
</div>
<div class="panel-body">
{% if result.avatar_server_http %}
<tt>{{result.avatar_server_http}}</tt>
<a href="{{result.avatar_server_http}}">
<h4>{{result.avatar_server_http}}</h4>
</a>
{% if result.avatar_server_http_ipv4 %}
<br>{{ result.avatar_server_http_ipv4 }}
{% else %}
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
<br><center>{{ result.avatar_server_http_ipv4 }}</center>
{% endif %}
{% if result.avatar_server_http_ipv6 %}
<br>{{ result.avatar_server_http_ipv6 }}
<br><center>{{ result.avatar_server_http_ipv6 }}</center>
{% endif %}
{% else %}
<i>{% trans 'use <tt>http://cdn.libravatar.org</tt>' %}</i>
<a href="http://cdn.libravatar.org">
<h4>http://cdn.libravatar.org</h4>
</a>
{% endif %}
</td>
</tr>
<tr>
<td valign="top" style="padding: 0px 10px 0px 10px;">{% trans 'HTTPS avatar server:' %}</td>
<td>
</div>
</div>
<div class="panel panel-tortin" style="width:intrinsic;margin-left:30px;float:left">
<div class="panel-heading">
<h3 class="panel-title"><i class="fa fa-lock"></i>&nbsp;HTTPS Server</h3>
</div>
<div class="panel-body">
{% if result.avatar_server_https %}
<tt>{{result.avatar_server_https}}</tt>
<a href="{{result.avatar_server_https}}">
<h4>{{result.avatar_server_https}}</h4>
</a>
{% if result.avatar_server_https_ipv4 %}
<br>{{ result.avatar_server_https_ipv4 }}
{% else %}
<br><strong>{% trans 'Warning: no A record for this hostname' %}</strong>
<br><center>{{ result.avatar_server_https_ipv4 }}</center>
{% endif %}
{% if result.avatar_server_https_ipv6 %}
<br>{{ result.avatar_server_https_ipv6 }}
<br><center>{{ result.avatar_server_https_ipv6 }}</center>
{% endif %}
{% else %}
<i>{% trans 'use <tt>https://seccdn.libravatar.org</tt>' %}</i>
<a href="https://seccdn.libravatar.org">
<h4>https://seccdn.libravatar.org</h4>
</a>
{% endif %}
</td>
</tr>
</table>
</div>
</div>
{% endif %}
</div>
<div style="height:40px"></div>

View File

@@ -66,15 +66,17 @@ class CheckView(FormView):
else:
default_url = None
if 'size' in form.cleaned_data:
size = form.cleaned_data['size']
if form.cleaned_data['mail']:
mailurl = libravatar_url(
email=form.cleaned_data['mail'],
size=form.cleaned_data['size'],
size=size,
default=default_url)
mailurl = mailurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
mailurl_secure = libravatar_url(
email=form.cleaned_data['mail'],
size=form.cleaned_data['size'],
size=size,
https=True,
default=default_url)
mailurl_secure = mailurl_secure.replace(
@@ -86,18 +88,20 @@ class CheckView(FormView):
hash_obj = hashlib.new('sha256')
hash_obj.update(form.cleaned_data['mail'].encode('utf-8'))
mail_hash256 = hash_obj.hexdigest()
size = form.cleaned_data['size']
mailurl_secure_256 = mailurl_secure.replace(
mail_hash,
mail_hash256)
if form.cleaned_data['openid']:
if not form.cleaned_data['openid'].startswith('http://') and not form.cleaned_data['openid'].startswith('https://'):
form.cleaned_data['openid'] = 'http://%s' % form.cleaned_data['openid']
openidurl = libravatar_url(
openid=form.cleaned_data['openid'],
size=form.cleaned_data['size'],
size=size,
default=default_url)
openidurl = openidurl.replace(LIBRAVATAR_BASE_URL, BASE_URL)
openidurl_secure = libravatar_url(
openid=form.cleaned_data['openid'],
size=form.cleaned_data['size'],
size=size,
https=True,
default=default_url)
openidurl_secure = openidurl_secure.replace(
@@ -106,13 +110,13 @@ class CheckView(FormView):
openid_hash = parse_user_identity(
openid=form.cleaned_data['openid'],
email=None)[0]
size = form.cleaned_data['size']
return render(self.request, self.template_name, {
'form': form,
'mailurl': mailurl,
'openidurl': openidurl,
'mailurl_secure': mailurl_secure,
'mailurl_secure_256': mailurl_secure_256,
'openidurl_secure': openidurl_secure,
'mail_hash': mail_hash,
'mail_hash256': mail_hash256,

View File

@@ -16,7 +16,9 @@ from django.urls import reverse_lazy
from PIL import Image
from monsterid.id import build_monster as BuildMonster
from pydenticon import Generator as IdenticonGenerator
import Identicon
from pydenticon5 import Pydenticon5
import pagan
from robohash import Robohash
from ivatar.settings import AVATAR_MAX_SIZE, JPEG_QUALITY, DEFAULT_AVATAR_SIZE
@@ -120,6 +122,14 @@ class AvatarImageView(TemplateView):
# Return the default URL, as specified, or 404 Not Found, if default=404
if default:
# Proxy to gravatar to generate wavatar - lazy me
if str(default) == 'wavatar':
url = reverse_lazy('gravatarproxy', args=[kwargs['digest']]) \
+ '?s=%i' % size + '&default=%s&f=y' % default
return HttpResponseRedirect(url)
if str(default) == str(404):
return HttpResponseNotFound(_('<h1>Image not found</h1>'))
@@ -145,32 +155,35 @@ class AvatarImageView(TemplateView):
data,
content_type='image/png')
if str(default) == 'identicon' or str(default) == 'retro':
# Taken from example code
foreground = [
'rgb(45,79,255)',
'rgb(254,180,44)',
'rgb(226,121,234)',
'rgb(30,179,253)',
'rgb(232,77,65)',
'rgb(49,203,115)',
'rgb(141,69,170)']
background = 'rgb(224,224,224)'
padwidth = int(size/10)
if padwidth < 10:
padwidth = 10
if size < 60:
padwidth = 0
padding = (padwidth, padwidth, padwidth, padwidth)
# Since padding is _added_ around the generated image, we
# need to reduce the image size by padding*2 (left/right, top/bottom)
size = size - 2*padwidth
generator = IdenticonGenerator(
10, 10, digest=hashlib.sha1,
foreground=foreground, background=background)
data = generator.generate(
kwargs['digest'], size, size,
output_format='png', padding=padding, inverted=False)
if str(default) == 'retro':
identicon = Identicon.render(kwargs['digest'])
data = BytesIO()
img = Image.open(BytesIO(identicon))
img = img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
data.seek(0)
return HttpResponse(
data,
content_type='image/png')
if str(default) == 'pagan':
paganobj = pagan.Avatar(kwargs['digest'])
data = BytesIO()
img = paganobj.img.resize((size, size), Image.ANTIALIAS)
img.save(data, 'PNG', quality=JPEG_QUALITY)
data.seek(0)
return HttpResponse(
data,
content_type='image/png')
if str(default) == 'identicon':
p = Pydenticon5()
# In order to make use of the whole 32 bytes digest, we need to redigest them.
newdigest = hashlib.md5(bytes(kwargs['digest'], 'utf-8')).hexdigest()
img = p.draw(newdigest, size, 0)
data = BytesIO()
img.save(data, 'PNG', quality=JPEG_QUALITY)
data.seek(0)
return HttpResponse(
data,
content_type='image/png')
@@ -194,7 +207,11 @@ class AvatarImageView(TemplateView):
imgformat = obj.photo.format
photodata = Image.open(BytesIO(obj.photo.data))
# If the image is smaller than what was requested, we need
# to use the function resize
if photodata.size[0] < size or photodata.size[1] < size:
photodata = photodata.resize((size, size), Image.ANTIALIAS)
else:
photodata.thumbnail((size, size), Image.ANTIALIAS)
data = BytesIO()
photodata.save(data, pil_format(imgformat), quality=JPEG_QUALITY)
@@ -235,6 +252,7 @@ class GravatarProxyView(View):
except:
pass
if str(default) != 'wavatar':
# This part is special/hackish
# Check if the image returned by Gravatar is their default image, if so,
# redirect to our default instead.
@@ -249,7 +267,7 @@ class GravatarProxyView(View):
print('Gravatar test url fetch failed: %s' % exc)
gravatar_url = 'https://secure.gravatar.com/avatar/' + kwargs['digest'] \
+ '?s=%i' % size
+ '?s=%i' % size + '&d=%s' % default
try:
gravatarimagedata = urlopen(gravatar_url, timeout=URL_TIMEOUT)

View File

@@ -33,6 +33,8 @@ mysqlclient
psycopg2
notsetuptools
git+https://github.com/ofalk/monsterid.git
git+https://github.com/azaghal/pydenticon.git
git+https://github.com/ofalk/Robohash.git@devel
python-memcached
git+https://github.com/ercpe/pydenticon5.git
git+https://github.com/flavono123/identicon.git
pagan

View File

@@ -45,8 +45,12 @@
</a>
<ul class="dropdown-menu" aria-labelledby="tools_dropdown">
<li><a id="tools-check" href="{% url 'tools_check' %}">
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check' %}
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check ID' %}
</a></li>
<li><a id="tools-check-domain" href="{% url 'tools_check_domain' %}">
<i class="fa fa-fw fa-check-square" aria-hidden="true"></i> {% trans 'Check Domain' %}
</a></li>
</ul>
</li>
</ul>