Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Printing #712

Merged
merged 8 commits into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 211 additions & 0 deletions ocfweb/account/print.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import base64
import os.path
import subprocess
import uuid
from email.message import EmailMessage
from email.utils import make_msgid
from io import BytesIO

import qrcode
from django import forms
from django.forms import widgets
from django.http import HttpRequest
from django.http import HttpResponse
from django.shortcuts import render
from ocflib.misc.mail import MAIL_FROM
from ocflib.misc.mail import SENDMAIL_PATH
from paramiko import AuthenticationException
from paramiko import SSHClient
from paramiko.hostkeys import HostKeyEntry

from ocfweb.component.forms import Form


PRINT_FOLDER = '.user_print'

EMAIL_BODY = '''
<html>
<body>
<p>
Hi {username},
</p>

<p>
Below you can find the QR code to start your print in the OCF computer lab.
Show the QR code in front of the camera at the remote printing computer,
and it will start printing your document.
</p>
<img src="cid:{image_cid}">
<p>
Thanks for flying OCF! <br>
~ OCF volunteer staff
</p>
</body>
</html>
'''


def send_qr_mail(username: str, qr_code: bytes) -> None:
"""Send the QR code needed to start the print in the lab to the user.

Based on https://stackoverflow.com/a/49098251/9688107
"""

msg = EmailMessage()
msg['Subject'] = 'OCF Remote Printing QR Code'
msg['From'] = MAIL_FROM
msg['To'] = f'{username}@ocf.berkeley.edu'

msg.set_content(
'Sorry, we were unable to display your QR, please use the QR code on the website!.',
)

image_cid = make_msgid(domain='ocf.berkeley.edu')
msg.add_alternative(EMAIL_BODY.format(username=username, image_cid=image_cid), subtype='html')
msg.get_payload()[1].add_related(
qr_code,
maintype='image',
subtype='png',
cid=image_cid,
)
# we send the message via sendmail because direct traffic to port 25
# is firewalled off
p = subprocess.Popen(
(SENDMAIL_PATH, '-t', '-oi'),
stdin=subprocess.PIPE,
)
p.communicate(msg.as_string().encode('utf8'))


def print(request: HttpRequest) -> HttpResponse:
if request.method == 'POST':
form = PrintForm(
request.POST,
request.FILES,
initial={
'double_or_single': 'single',
},
)
qr_b64 = b''
double_or_single = 'single'
error = ''
if form.is_valid():
username = form.cleaned_data['username']
password = form.cleaned_data['password']

file = request.FILES['file']

double_or_single = form.cleaned_data['double_or_single']

ssh = SSHClient()

host_keys = ssh.get_host_keys()
entry = HostKeyEntry.from_line(
'ssh.ocf.berkeley.edu ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAqMkHVVoMl8md25iky7e2Xe3ARaC4H1PbIpv5Y+xT4KOT17gGvFSmfjGyW9P8ZTyqxq560iWdyELIn7efaGPbkUo9retcnT6WLmuh9nRIYwb6w7BGEEvlblBmH27Fkgt7JQ6+1sr5teuABfIMg22WTQAeDQe1jg0XsPu36OjbC7HjA3BXsiNBpxKDolYIXWzOD+r9FxZLP0lawh8dl//O5FW4ha1IbHklq2i9Mgl79wAH3jxf66kQJTvLmalKnQ0Dbp2+vYGGhIjVFXlGSzKsHAVhuVD6TBXZbxWOYoXanS7CC43MrEtBYYnc6zMn/k/rH0V+WeRhuzTnr/OZGJbBBw==', # noqa
)
assert entry is not None # should never be none as we are passing a static string above
host_keys.add(
'ssh.ocf.berkeley.edu',
'ssh-rsa',
entry.key,
)

try:
ssh.connect(
'ssh.ocf.berkeley.edu',
username=username,
password=password,
)
except AuthenticationException:
error = 'Authentication failed. Did you type the wrong username or password?'

if not error:
sftp = ssh.open_sftp()
try:
folder = sftp.stat(PRINT_FOLDER)
if folder.FLAG_PERMISSIONS != 0o700:
sftp.chmod(PRINT_FOLDER, 0o700)
except FileNotFoundError:
sftp.mkdir(PRINT_FOLDER, 0o700)
try:
rid = uuid.uuid4().hex
filename = f'{rid}-{file.name}'
with sftp.open(os.path.join(PRINT_FOLDER, filename), 'wb+') as dest:
for chunk in file.chunks():
dest.write(chunk)
except OSError as e:
error = 'Failed to open file in user home directory, ' + \
f'please report this to [email protected] with the traceback\n{e}'
else:
qr = qrcode.QRCode(
version=1,
box_size=10,
border=5,
)
qr.add_data(f'{username}:{filename}:{double_or_single}')
qr.make(fit=True)
img = qr.make_image(fill='black', back_color='white')
buff = BytesIO()
img.save(buff, format='PNG')
qr_data = buff.getvalue()
qr_b64 = b'data:image/png;base64,%b' % base64.b64encode(qr_data)
send_qr_mail(
username=username,
qr_code=qr_data,
)
return render(
request,
'account/print/qr.html',
{
'title': 'QR Code',
'qr_b64': qr_b64.decode('utf-8'),
'error': error,
},
)
else:
form = PrintForm(
initial={
'double_or_single': 'single',
},
)

return render(
request,
'account/print/index.html', {
'title': 'Print remotely',
'form': form,
},
)


class PrintForm(Form):
username = forms.CharField(
label='OCF username',
min_length=3,
max_length=16,
)
password = forms.CharField(
widget=forms.PasswordInput,
label='Password',
min_length=8,
max_length=256,
)

file = forms.FileField()

PRINT_CHOICES = (
(
'single',
'single sided -- one page per piece of paper',
),
(
'double',
'double sided -- two pages per piece of paper',
),
)

double_or_single = forms.ChoiceField(
choices=PRINT_CHOICES,
label='Print double or single sided',
widget=widgets.RadioSelect,
)
21 changes: 21 additions & 0 deletions ocfweb/account/templates/account/print/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load bootstrap %}

{% block content %}
<div class="row">
<div class="col-md-8 ocf-content-block">
<h3>Print remotely</h3>
<p>
Please select a PDF file you would like to print and whether you would like to print single or double sided.

Once you press submit, a QR code will displayed that you should bring to the OCF computer lab.
</p>

<form action="{% url 'print' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{{form | bootstrap}}
<input class="btn btn-primary" type="submit" value="Submit print to queue" />
</form>
</div>
</div>
{% endblock %}
21 changes: 21 additions & 0 deletions ocfweb/account/templates/account/print/qr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% load bootstrap %}

{% block content %}
<div class="row">
<div class="col-md-8 ocf-content-block">
<p>
Please bring this QR code to the lab to start your print job. A copy was emailed to you.
</p>
{% if qr_b64 %}
<div class="well">
<img alt="QR code to print" src="{{ qr_b64 }}" />
</div>
{% endif %}

{% if error %}
<p><strong>{{ error|linebreaks }}</strong></p>
{% endif %}
</div>
</div>
{% endblock %}
2 changes: 2 additions & 0 deletions ocfweb/account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from ocfweb.account.chpass import change_password
from ocfweb.account.commands import commands
from ocfweb.account.print import print
from ocfweb.account.register import account_created
from ocfweb.account.register import account_pending
from ocfweb.account.register import recommend
Expand All @@ -19,6 +20,7 @@
urlpatterns = [
url(r'^password/$', change_password, name='change_password'),
url(r'^commands/$', commands, name='commands'),
url(r'^print/$', print, name='print'),

# account creation
url(r'^register/$', request_account, name='register'),
Expand Down
1 change: 1 addition & 0 deletions ocfweb/main/templates/main/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ <h3>OCF Computer Lab</h3>
<ul>
<li><a href="{% url 'commands' %}">Check Account</a></li>
<li><a href="{% url 'change_password' %}">Reset Password</a></li>
<li><a href="{% url 'print' %}">Print Remotely</a></li>

{% if is_ocf_ip %}
<li><a href="https://printhost.ocf.berkeley.edu/jobs/">View Print Queue</a></li>
Expand Down
1 change: 1 addition & 0 deletions ocfweb/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@

<li><a href="{% url 'commands' %}">Commands</a></li>
<li><a href="{% url 'change_password' %}">Change Password</a></li>
<li><a href="{% url 'print' %}">Print Remotely</a></li>
<li><a href="{% url 'logout' %}?next={{request_full_path}}">Log Out</a></li>
</ul>
</li>
Expand Down
7 changes: 7 additions & 0 deletions requirements-dev-minimal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ pytest-django
pytest-env
pytest-xdist
requirements-tools
types-cryptography
types-enum34
types-ipaddress
types-paramiko
types-python-dateutil
types-pytz
types-PyYAML
types-requests
types-setuptools
types-toml
2 changes: 2 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ types-python-dateutil==2.8.0
types-pytz==2021.1.2
types-PyYAML==5.4.10
types-requests==2.25.6
types-setuptools==57.0.2
types-toml==0.1.5
virtualenv==20.7.2
2 changes: 2 additions & 0 deletions requirements-minimal.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ mistune
numpy
ocflib
paramiko
pillow
pyasn1
pycryptodome
pygments
pymysql
python-dateutil
qrcode
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pysnmp==4.4.12
python-dateutil==2.8.2
pytz==2021.1
PyYAML==5.4.1
qrcode==7.3
redis==3.5.3
requests==2.24.0
setuptools==58.0.4
Expand Down