Skip to content

Commit 19e7d0e

Browse files
committed
Added code to get SSL expiry date so that we can tell people when their certificates expire. by: Giles
1 parent 81f4364 commit 19e7d0e

File tree

4 files changed

+83
-2
lines changed

4 files changed

+83
-2
lines changed

pythonanywhere/api.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from textwrap import dedent
21
import getpass
32
import os
4-
from pathlib import Path
53
import requests
4+
from datetime import datetime
5+
from textwrap import dedent
6+
from pathlib import Path
67

78
from pythonanywhere.exceptions import SanityException
89
from pythonanywhere.snakesay import snakesay
@@ -157,3 +158,19 @@ def set_ssl(self, certificate, private_key):
157158
response_text=response.text,
158159
)
159160
)
161+
162+
163+
def get_ssl_info(self):
164+
url = get_api_endpoint().format(username=getpass.getuser()) + self.domain + '/ssl/'
165+
response = call_api(url, 'get')
166+
if not response.ok:
167+
raise Exception(
168+
'GET SSL details via API failed, got {response}:{response_text}'.format(
169+
response=response,
170+
response_text=response.text,
171+
)
172+
)
173+
174+
result = response.json()
175+
result["not_after"] = datetime.strptime(result["not_after"], "%Y%m%dT%H%M%SZ")
176+
return result

scripts/pa_install_webapp_letsencrypt_ssl.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import sys
1919

2020
from pythonanywhere.api import Webapp
21+
from pythonanywhere.snakesay import snakesay
2122

2223

2324
def main(domain_name, suppress_reload):
@@ -40,6 +41,18 @@ def main(domain_name, suppress_reload):
4041
if not suppress_reload:
4142
webapp.reload()
4243

44+
ssl_details = webapp.get_ssl_info()
45+
print(snakesay(
46+
"That's all set up now :-)\n"
47+
"Your new certificate will expire on {expiry:%d %B %Y},\n"
48+
"so shortly before then you should renew it\n"
49+
"(see https://help.pythonanywhere.com/pages/LetsEncrypt/)\n"
50+
"and install the new certificate".format(
51+
expiry=ssl_details["not_after"]
52+
)
53+
))
54+
55+
4356
done = True
4457
break
4558

scripts/pa_install_webapp_ssl.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import sys
2121

2222
from pythonanywhere.api import Webapp
23+
from pythonanywhere.snakesay import snakesay
2324

2425

2526
def main(domain_name, certificate_file, private_key_file, suppress_reload):
@@ -40,6 +41,16 @@ def main(domain_name, certificate_file, private_key_file, suppress_reload):
4041
if not suppress_reload:
4142
webapp.reload()
4243

44+
ssl_details = webapp.get_ssl_info()
45+
print(snakesay(
46+
"That's all set up now :-)\n"
47+
"Your new certificate will expire on {expiry:%d %B %Y},\n"
48+
"so shortly before then you should renew it\n"
49+
"and install the new certificate".format(
50+
expiry=ssl_details["not_after"]
51+
)
52+
))
53+
4354

4455
if __name__ == '__main__':
4556
arguments = docopt(__doc__)

tests/test_api.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33
import pytest
44
import responses
5+
from datetime import datetime
56
from unittest.mock import patch
67
from urllib.parse import urlencode
78

@@ -288,3 +289,42 @@ def test_raises_if_post_does_not_20x(self, api_responses, api_token):
288289

289290
assert 'POST to set SSL details via API failed' in str(e.value)
290291
assert 'nope' in str(e.value)
292+
293+
294+
class TestGetWebappSSLInfo:
295+
296+
def test_returns_json_from_server_having_parsed_expiry(self, api_responses, api_token):
297+
expected_url = get_api_endpoint().format(username=getpass.getuser()) + 'mydomain.com/ssl/'
298+
api_responses.add(
299+
responses.GET, expected_url, status=200,
300+
body=json.dumps({
301+
"not_after": "20180824T171623Z",
302+
"issuer_name": "PythonAnywhere test CA",
303+
"subject_name": "www.mycoolsite.com",
304+
"subject_alternate_names": ["www.mycoolsite.com", "mycoolsite.com"],
305+
})
306+
)
307+
308+
assert Webapp('mydomain.com').get_ssl_info() == {
309+
"not_after": datetime(2018, 8, 24, 17, 16, 23),
310+
"issuer_name": "PythonAnywhere test CA",
311+
"subject_name": "www.mycoolsite.com",
312+
"subject_alternate_names": ["www.mycoolsite.com", "mycoolsite.com"],
313+
}
314+
315+
316+
get = api_responses.calls[0]
317+
assert get.request.method == 'GET'
318+
assert get.request.url == expected_url
319+
assert get.request.headers['Authorization'] == 'Token {api_token}'.format(api_token=api_token)
320+
321+
322+
def test_raises_if_get_does_not_return_200(self, api_responses, api_token):
323+
expected_url = get_api_endpoint().format(username=getpass.getuser()) + 'mydomain.com/ssl/'
324+
api_responses.add(responses.GET, expected_url, status=404, body='nope')
325+
326+
with pytest.raises(Exception) as e:
327+
Webapp('mydomain.com').get_ssl_info()
328+
329+
assert 'GET SSL details via API failed, got' in str(e.value)
330+
assert 'nope' in str(e.value)

0 commit comments

Comments
 (0)