Skip to content

Commit c178740

Browse files
committed
Add support for self-signed certificates
1 parent 16b003f commit c178740

5 files changed

Lines changed: 82 additions & 25 deletions

File tree

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2016-2022 Frederic Guillot
3+
Copyright (c) Frederic Guillot
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.rst

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Installation
1212

1313
.. code-block:: bash
1414
15-
pip install kanboard
15+
python3 -m pip install kanboard
1616
1717
1818
This library is compatible with Python >= 3.5.
@@ -32,7 +32,7 @@ Examples
3232
Methods and arguments are the same as the JSON-RPC procedures described in the
3333
`official documentation <https://docs.kanboard.org/v1/api/>`_.
3434

35-
Python methods are dynamically mapped to the API procedures. **You must use named arguments.**
35+
Python methods are dynamically mapped to the API procedures: **You must use named arguments**.
3636

3737
By default, calls are made synchronously, meaning that they will block the program until completed.
3838

@@ -68,6 +68,49 @@ Create a new task
6868
project_id = kb.create_project(name='My project')
6969
task_id = kb.create_task(project_id=project_id, title='My task title')
7070
71+
SSL connection and self-signed certificates
72+
===========================================
73+
74+
Example with a valid certificate:
75+
76+
.. code-block:: python
77+
78+
import kanboard
79+
80+
kb = kanboard.Client('https://example.org/jsonrpc.php', 'admin', 'secret')
81+
kb.get_my_projects()
82+
83+
Example with a custom certificate:
84+
85+
.. code-block:: python
86+
87+
import kanboard
88+
89+
kb = kanboard.Client('https://example.org/jsonrpc.php', 'admin', 'secret', cafile='/path/to/my/cert.pem')
90+
kb.get_my_projects()
91+
92+
Example with a custom certificate and hostname mismatch:
93+
94+
.. code-block:: python
95+
96+
import kanboard
97+
98+
kb = kanboard.Client(url='https://example.org/jsonrpc.php',
99+
username='admin',
100+
password='secret',
101+
cafile='/path/to/my/cert.pem',
102+
ignore_hostname_verification=True)
103+
kb.get_my_projects()
104+
105+
Ignore invalid/expired certificates and hostname mismatches, which will make your application vulnerable to man-in-the-middle (MitM) attacks:
106+
107+
.. code-block:: python
108+
109+
import kanboard
110+
111+
kb = kanboard.Client('https://example.org/jsonrpc.php', 'admin', 'secret', insecure=True)
112+
kb.get_my_projects()
113+
71114
Asynchronous I/O
72115
================
73116

kanboard.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The MIT License (MIT)
22
#
3-
# Copyright (c) 2016-2022 Frederic Guillot
3+
# Copyright (c) Frederic Guillot
44
#
55
# Permission is hereby granted, free of charge, to any person obtaining a copy
66
# of this software and associated documentation files (the "Software"), to deal
@@ -24,6 +24,8 @@
2424
import base64
2525
import functools
2626
import asyncio
27+
import ssl
28+
from typing import Dict, Optional
2729
from urllib import request as http
2830

2931

@@ -52,12 +54,14 @@ class Client:
5254
"""
5355

5456
def __init__(self,
55-
url,
56-
username,
57-
password,
58-
auth_header=DEFAULT_AUTH_HEADER,
59-
cafile=None,
60-
loop=None):
57+
url: str,
58+
username: str,
59+
password: str,
60+
auth_header: str = DEFAULT_AUTH_HEADER,
61+
cafile: Optional[str] = None,
62+
insecure: bool = False,
63+
ignore_hostname_verification: bool = False,
64+
loop: Optional[asyncio.AbstractEventLoop] = None):
6165
"""
6266
Constructor
6367
@@ -66,22 +70,26 @@ def __init__(self,
6670
username: API username or real username
6771
password: API token or user password
6872
auth_header: API HTTP header
69-
cafile: path to a custom CA certificate
70-
loop: an asyncio event loop. Default: asyncio.get_event_loop()
73+
cafile: Path to a custom CA certificate
74+
insecure: Ignore SSL certificate errors and ignore hostname mismatches
75+
ignore_hostname_verification: Ignore SSL certificate hostname verification
76+
loop: An asyncio event loop. Default: asyncio.get_event_loop()
7177
"""
7278
self._url = url
7379
self._username = username
7480
self._password = password
7581
self._auth_header = auth_header
7682
self._cafile = cafile
83+
self._insecure = insecure
84+
self._ignore_hostname_verification = ignore_hostname_verification
7785

7886
if not loop:
7987
try:
8088
self._event_loop = asyncio.get_event_loop()
8189
except RuntimeError:
8290
self._event_loop = asyncio.new_event_loop()
8391

84-
def __getattr__(self, name):
92+
def __getattr__(self, name: str):
8593
if self.is_async_method_name(name):
8694
async def function(*args, **kwargs):
8795
return await self._event_loop.run_in_executor(
@@ -96,20 +104,20 @@ def function(*args, **kwargs):
96104
return function
97105

98106
@staticmethod
99-
def is_async_method_name(funcname):
107+
def is_async_method_name(funcname: str) -> bool:
100108
return funcname.endswith(ASYNC_FUNCNAME_MARKER)
101109

102110
@staticmethod
103-
def get_funcname_from_async_name(funcname):
111+
def get_funcname_from_async_name(funcname: str) -> str:
104112
return funcname[:len(funcname) - len(ASYNC_FUNCNAME_MARKER)]
105113

106114
@staticmethod
107-
def _to_camel_case(snake_str):
115+
def _to_camel_case(snake_str: str) -> str:
108116
components = snake_str.split('_')
109117
return components[0] + ''.join(x.title() for x in components[1:])
110118

111119
@staticmethod
112-
def _parse_response(response):
120+
def _parse_response(response: bytes):
113121
try:
114122
body = json.loads(response.decode(errors='ignore'))
115123

@@ -121,20 +129,26 @@ def _parse_response(response):
121129
except ValueError:
122130
return None
123131

124-
def _do_request(self, headers, body):
132+
def _do_request(self, headers: Dict[str, str], body: Dict):
125133
try:
126134
request = http.Request(self._url,
127135
headers=headers,
128136
data=json.dumps(body).encode())
129-
if self._cafile:
130-
response = http.urlopen(request, cafile=self._cafile).read()
131-
else:
132-
response = http.urlopen(request).read()
137+
138+
ssl_context = ssl.create_default_context(cafile=self._cafile)
139+
if self._insecure:
140+
ssl_context.check_hostname = False
141+
ssl_context.verify_mode = ssl.CERT_NONE
142+
143+
if self._ignore_hostname_verification:
144+
ssl_context.check_hostname = False
145+
146+
response = http.urlopen(request, context=ssl_context).read()
133147
except Exception as e:
134148
raise ClientError(str(e))
135149
return self._parse_response(response)
136150

137-
def execute(self, method, **kwargs):
151+
def execute(self, method: str, **kwargs):
138152
"""
139153
Call remote API procedure
140154

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The MIT License (MIT)
22
#
3-
# Copyright (c) 2016-2022 Frederic Guillot
3+
# Copyright (c) Frederic Guillot
44
#
55
# Permission is hereby granted, free of charge, to any person obtaining a copy
66
# of this software and associated documentation files (the "Software"), to deal

test_kanboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The MIT License (MIT)
22
#
3-
# Copyright (c) 2016-2022 Frederic Guillot
3+
# Copyright (c) Frederic Guillot
44
#
55
# Permission is hereby granted, free of charge, to any person obtaining a copy
66
# of this software and associated documentation files (the "Software"), to deal

0 commit comments

Comments
 (0)