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

Add support for client certificates #85

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
12 changes: 9 additions & 3 deletions tusclient/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Optional
from typing import Dict, Optional, Tuple, Union

from tusclient.uploader import Uploader, AsyncUploader

Expand All @@ -16,15 +16,21 @@ class TusClient:
along with every request made by the cleint to the server. This may be used to set
authentication headers. These headers should not include headers required by tus
protocol. If not set this defaults to an empty dictionary.

- client_cert (str|tuple[str,str]):
Path of PEM encoded client certitifacate and optionally path to PEM encoded
key file. The PEM encoded key of the certificate can either be included in the
certificate itself or be provided in a seperate file.
Only unencrypted keys are supported!
:Constructor Args:
- url (str)
- headers (Optiional[dict])
- client_cert (Optional[str | Tuple[str, str]])
"""

def __init__(self, url: str, headers: Optional[Dict[str, str]] = None):
def __init__(self, url: str, headers: Optional[Dict[str, str]] = None, client_cert: Optional[Union[str, Tuple[str, str]]] = None):
self.url = url
self.headers = headers or {}
self.client_cert = client_cert

def set_headers(self, headers: Dict[str, str]):
"""
Expand Down
17 changes: 14 additions & 3 deletions tusclient/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import requests
import aiohttp
import ssl

from tusclient.exceptions import TusUploadFailed, TusCommunicationError

Expand Down Expand Up @@ -46,6 +47,7 @@ def __init__(self, uploader):
self.verify_tls_cert = bool(uploader.verify_tls_cert)
self.file = uploader.get_file_stream()
self.file.seek(uploader.offset)
self.client_cert = uploader.client_cert

self._request_headers = {
"upload-offset": str(uploader.offset),
Expand Down Expand Up @@ -84,6 +86,8 @@ def perform(self):
data=chunk,
headers=self._request_headers,
verify=self.verify_tls_cert,
stream=True,
cert=self.client_cert
)
self.status_code = resp.status_code
self.response_content = resp.content
Expand All @@ -108,10 +112,17 @@ async def perform(self):
chunk = self.file.read(self._content_length)
self.add_checksum(chunk)
try:
async with aiohttp.ClientSession(loop=self.io_loop) as session:
ssl = None if self.verify_tls_cert else False
ssl_ctx = ssl.create_default_context()
if (self.client_cert is not None):
if self.client_cert is str:
ssl_ctx.load_cert_chain(certfile=self.client_cert)
else:
ssl_ctx.load_cert_chain(certfile=self.client_cert[0], keyfile=self.client_cert[1])
conn = aiohttp.TCPConnector(ssl=ssl_ctx)
async with aiohttp.ClientSession(loop=self.io_loop, connector=conn) as session:
verify_tls_cert = None if self.verify_tls_cert else False
async with session.patch(
self._url, data=chunk, headers=self._request_headers, ssl=ssl
self._url, data=chunk, headers=self._request_headers, ssl=verify_tls_cert
) as resp:
self.status_code = resp.status
self.response_headers = {
Expand Down
9 changes: 7 additions & 2 deletions tusclient/uploader/baseuploader.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, IO, Dict, TYPE_CHECKING
from typing import Optional, IO, Dict, Tuple, TYPE_CHECKING, Union
import os
import re
from base64 import b64encode
Expand Down Expand Up @@ -177,6 +177,11 @@ def checksum_algorithm_name(self):
"""
return self.__checksum_algorithm_name

@property
def client_cert(self):
"""The client certificate used for the configured client"""
return self.client.client_cert if self.client is not None else None

@catch_requests_error
def get_offset(self):
"""
Expand All @@ -186,7 +191,7 @@ def get_offset(self):
http request to the tus server to retrieve the offset.
"""
resp = requests.head(
self.url, headers=self.get_headers(), verify=self.verify_tls_cert
self.url, headers=self.get_headers(), verify=self.verify_tls_cert, cert=self.client_cert
)
offset = resp.headers.get("upload-offset")
if offset is None:
Expand Down
15 changes: 12 additions & 3 deletions tusclient/uploader/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import requests
import aiohttp
import ssl

from tusclient.uploader.baseuploader import BaseUploader

Expand Down Expand Up @@ -70,6 +71,7 @@ def create_url(self):
self.client.url,
headers=self.get_url_creation_headers(),
verify=self.verify_tls_cert,
cert=self.client_cert,
)
url = resp.headers.get("location")
if url is None:
Expand Down Expand Up @@ -149,11 +151,18 @@ async def create_url(self):
Makes request to tus server to create a new upload url for the required file upload.
"""
try:
async with aiohttp.ClientSession() as session:
ssl_ctx = ssl.create_default_context()
if (self.client_cert is not None):
if self.client_cert is str:
ssl_ctx.load_cert_chain(certfile=self.client_cert)
else:
ssl_ctx.load_cert_chain(certfile=self.client_cert[0], keyfile=self.client_cert[1])
conn = aiohttp.TCPConnector(ssl=ssl_ctx)
async with aiohttp.ClientSession(connector=conn) as session:
headers = self.get_url_creation_headers()
ssl = None if self.verify_tls_cert else False
verify_tls_cert = None if self.verify_tls_cert else False
async with session.post(
self.client.url, headers=headers, ssl=ssl
self.client.url, headers=headers, ssl=verify_tls_cert
) as resp:
url = resp.headers.get("location")
if url is None:
Expand Down