kicost-digikey-api-v4 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kicost_digikey_api_v4-0.1.3/MANIFEST.in +1 -0
- kicost_digikey_api_v4-0.1.3/PKG-INFO +65 -0
- kicost_digikey_api_v4-0.1.3/README.md +47 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/__init__.py +20 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/certauth/__init__.py +0 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/certauth/certauth.py +421 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/exceptions.py +10 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/oauth/__init__.py +0 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/oauth/oauth2.py +306 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/utils.py +185 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/__init__.py +0 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/api.py +145 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/__init__.py +79 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/api/__init__.py +6 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/api/product_search_api.py +1722 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/api_client.py +632 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/configuration.py +263 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/__init__.py +73 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/alternate_packaging.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/alternate_packaging_response.py +136 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/base_filter_v4.py +162 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/base_product.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/break_price.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/categories_response.py +166 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/category.py +224 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/category_node.py +308 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/category_response.py +136 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/category_type.py +168 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/classifications.py +224 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/description.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/digi_reel_pricing.py +222 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/dk_problem_details.py +266 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/filter_id.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/filter_options.py +287 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/filter_options_request.py +347 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/filter_value.py +202 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/filters.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/iso_search_locale.py +168 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/keyword_request.py +220 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/keyword_response.py +242 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/manufacturer.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/manufacturer_info.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/manufacturers_response.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/media_links.py +224 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/media_response.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/package_type.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/package_type_by_quantity_product.py +726 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/package_type_by_quantity_response.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/parameter.py +168 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/parameter_filter_options_response.py +222 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/parameter_filter_request.py +138 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/parameter_value.py +230 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/parametric_category.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/price_break.py +168 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product.py +766 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_associations.py +196 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_associations_response.py +136 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_details.py +136 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_pricing.py +668 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_pricing_response.py +166 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_pricing_variation.py +306 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_status_v4.py +136 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_substitute.py +278 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_substitutes_response.py +166 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_summary.py +250 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/product_variation.py +416 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/recommendation.py +166 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/recommended_product.py +308 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/recommended_products_response.py +112 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/series.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/settings_used.py +164 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/sort_direction.py +93 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/sort_option.py +99 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/sort_options.py +152 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/supplier.py +140 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/top_category.py +192 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/models/top_category_node.py +168 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4/v4/productinformation/rest.py +317 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4.egg-info/PKG-INFO +65 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4.egg-info/SOURCES.txt +85 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4.egg-info/dependency_links.txt +1 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4.egg-info/requires.txt +9 -0
- kicost_digikey_api_v4-0.1.3/kicost_digikey_api_v4.egg-info/top_level.txt +1 -0
- kicost_digikey_api_v4-0.1.3/requirements.txt +9 -0
- kicost_digikey_api_v4-0.1.3/setup.cfg +25 -0
- kicost_digikey_api_v4-0.1.3/setup.py +40 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include requirements.txt
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: kicost_digikey_api_v4
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: KiCost plugin for the Digikey PartSearch API
|
|
5
|
+
Home-page: https://github.com/set-soft/kicost-digikey-api-v4
|
|
6
|
+
Author: Salvador E. Tropea
|
|
7
|
+
Author-email: stropea@inti.gob.ar
|
|
8
|
+
License: GPL v3
|
|
9
|
+
Description: KiCost plug-in for Digi-Key API
|
|
10
|
+
=================================
|
|
11
|
+
This is an experimental plug-in to allow KiCost to do native Digi-Key API requests.
|
|
12
|
+
|
|
13
|
+
The plug-in is based on the [Python Client for Digikey API](https://github.com/peeter123/digikey-api)
|
|
14
|
+
by [Peter Oostewechel](https://github.com/peeter123).
|
|
15
|
+
|
|
16
|
+
I'm not using the original project as a module because it pulls some rare dependencies.
|
|
17
|
+
|
|
18
|
+
The license is GPL v3 as the original work.
|
|
19
|
+
|
|
20
|
+
# Quickstart
|
|
21
|
+
|
|
22
|
+
- Register at Digi-Key. You need a valid user in order to register an application and in order to authenticate.
|
|
23
|
+
- Go to the API portal: [Digi-Key API Solutions](https://developer.digikey.com/get_started)
|
|
24
|
+
- Login to Digi-Key from the API portal
|
|
25
|
+
- Create an [organization](https://developer.digikey.com/teams)
|
|
26
|
+
- Choose the `Production Apps` operation
|
|
27
|
+
- Create a production app
|
|
28
|
+
- Choose a `Production App name`
|
|
29
|
+
- Use `https://localhost:8139/digikey_callback` for the OAuth Callback. So you can authorize the use of the app.
|
|
30
|
+
- Give a description
|
|
31
|
+
- Enable the `Product Information` API
|
|
32
|
+
- Click `Add production app`
|
|
33
|
+
- Now select your newly created `Production App`
|
|
34
|
+
- In the `Credential` section enable the options to show the `Client ID` and `Client Secret`
|
|
35
|
+
- Copy these values to a file named `config.txt` containing:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
DIGIKEY_CLIENT_ID = Client_ID_Value_for_your_app
|
|
39
|
+
DIGIKEY_CLIENT_SECRET = Client_Secret_Value_for_your_app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Create a folder `~/.config/kicost_digikey_api_v4`
|
|
43
|
+
- Store the config.txt file
|
|
44
|
+
- Now install the plug-in:
|
|
45
|
+
- Clone the git repo somewhere using `git clone https://github.com/set-soft/kicost-digikey-api-v4.git`
|
|
46
|
+
- Enter to `kicost-digikey-api-v4`
|
|
47
|
+
- Install it using `pip3 install -e .`
|
|
48
|
+
- Now test the plug-in:
|
|
49
|
+
- run `python3 test_production.py`
|
|
50
|
+
- A browser window will pop-up asking to login to Digi-Key. Login.
|
|
51
|
+
- Choose the allow option to enable the token.
|
|
52
|
+
- Now you'll load a local page, allow it.
|
|
53
|
+
- Now the token is stored on disk and you won't need to confirm it for months.
|
|
54
|
+
- You should get the information for Digi-Key part 296-6501-6-ND
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Platform: UNKNOWN
|
|
58
|
+
Classifier: Development Status :: 3 - Alpha
|
|
59
|
+
Classifier: Environment :: Web Environment
|
|
60
|
+
Classifier: Intended Audience :: Developers
|
|
61
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
62
|
+
Classifier: Programming Language :: Python :: 2.7
|
|
63
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
64
|
+
Classifier: Topic :: Software Development
|
|
65
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
KiCost plug-in for Digi-Key API
|
|
2
|
+
=================================
|
|
3
|
+
This is an experimental plug-in to allow KiCost to do native Digi-Key API requests.
|
|
4
|
+
|
|
5
|
+
The plug-in is based on the [Python Client for Digikey API](https://github.com/peeter123/digikey-api)
|
|
6
|
+
by [Peter Oostewechel](https://github.com/peeter123).
|
|
7
|
+
|
|
8
|
+
I'm not using the original project as a module because it pulls some rare dependencies.
|
|
9
|
+
|
|
10
|
+
The license is GPL v3 as the original work.
|
|
11
|
+
|
|
12
|
+
# Quickstart
|
|
13
|
+
|
|
14
|
+
- Register at Digi-Key. You need a valid user in order to register an application and in order to authenticate.
|
|
15
|
+
- Go to the API portal: [Digi-Key API Solutions](https://developer.digikey.com/get_started)
|
|
16
|
+
- Login to Digi-Key from the API portal
|
|
17
|
+
- Create an [organization](https://developer.digikey.com/teams)
|
|
18
|
+
- Choose the `Production Apps` operation
|
|
19
|
+
- Create a production app
|
|
20
|
+
- Choose a `Production App name`
|
|
21
|
+
- Use `https://localhost:8139/digikey_callback` for the OAuth Callback. So you can authorize the use of the app.
|
|
22
|
+
- Give a description
|
|
23
|
+
- Enable the `Product Information` API
|
|
24
|
+
- Click `Add production app`
|
|
25
|
+
- Now select your newly created `Production App`
|
|
26
|
+
- In the `Credential` section enable the options to show the `Client ID` and `Client Secret`
|
|
27
|
+
- Copy these values to a file named `config.txt` containing:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
DIGIKEY_CLIENT_ID = Client_ID_Value_for_your_app
|
|
31
|
+
DIGIKEY_CLIENT_SECRET = Client_Secret_Value_for_your_app
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Create a folder `~/.config/kicost_digikey_api_v4`
|
|
35
|
+
- Store the config.txt file
|
|
36
|
+
- Now install the plug-in:
|
|
37
|
+
- Clone the git repo somewhere using `git clone https://github.com/set-soft/kicost-digikey-api-v4.git`
|
|
38
|
+
- Enter to `kicost-digikey-api-v4`
|
|
39
|
+
- Install it using `pip3 install -e .`
|
|
40
|
+
- Now test the plug-in:
|
|
41
|
+
- run `python3 test_production.py`
|
|
42
|
+
- A browser window will pop-up asking to login to Digi-Key. Login.
|
|
43
|
+
- Choose the allow option to enable the token.
|
|
44
|
+
- Now you'll load a local page, allow it.
|
|
45
|
+
- Now the token is stored on disk and you won't need to confirm it for months.
|
|
46
|
+
- You should get the information for Digi-Key part 296-6501-6-ND
|
|
47
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from kicost_digikey_api_v4.v4.api import (keyword_search, product_details, digi_reel_pricing, suggested_parts)
|
|
3
|
+
from .utils import by_digikey_pn, by_manf_pn, by_keyword, DK_API
|
|
4
|
+
from .exceptions import DigikeyError
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logger(logger_ref):
|
|
10
|
+
logger_ref.setLevel(logging.DEBUG)
|
|
11
|
+
formatter = logging.Formatter(
|
|
12
|
+
'%(asctime)s - %(name)20.20s - %(levelname)8s: %(message)s')
|
|
13
|
+
handler = logging.StreamHandler()
|
|
14
|
+
handler.setFormatter(formatter)
|
|
15
|
+
logger_ref.addHandler(handler)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
setup_logger(logger)
|
|
19
|
+
|
|
20
|
+
name = 'digikey'
|
|
File without changes
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
from OpenSSL import crypto
|
|
7
|
+
from OpenSSL.SSL import FILETYPE_PEM
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
import ipaddress
|
|
12
|
+
import tldextract
|
|
13
|
+
|
|
14
|
+
from argparse import ArgumentParser
|
|
15
|
+
|
|
16
|
+
from collections import OrderedDict
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
# =================================================================
|
|
21
|
+
# Valid for 3 years from now
|
|
22
|
+
# Max validity is 39 months:
|
|
23
|
+
# https://casecurity.org/2015/02/19/ssl-certificate-validity-periods-limited-to-39-months-starting-in-april/
|
|
24
|
+
CERT_NOT_AFTER = 3 * 365 * 24 * 60 * 60
|
|
25
|
+
|
|
26
|
+
CERTS_DIR = './ca/certs/'
|
|
27
|
+
|
|
28
|
+
CERT_NAME = 'certauth sample CA'
|
|
29
|
+
|
|
30
|
+
DEF_HASH_FUNC = 'sha256'
|
|
31
|
+
|
|
32
|
+
ROOT_CA = '!!root_ca'
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# =================================================================
|
|
36
|
+
class CertificateAuthority(object):
|
|
37
|
+
"""
|
|
38
|
+
Utility class for signing individual certificate
|
|
39
|
+
with a root cert.
|
|
40
|
+
|
|
41
|
+
Static generate_ca_root() method for creating the root cert
|
|
42
|
+
|
|
43
|
+
All certs saved on filesystem. Individual certs are stored
|
|
44
|
+
in specified certs_dir and reused if previously created.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, ca_name,
|
|
48
|
+
ca_file_cache,
|
|
49
|
+
cert_cache=None,
|
|
50
|
+
cert_not_before=0,
|
|
51
|
+
cert_not_after=CERT_NOT_AFTER,
|
|
52
|
+
overwrite=False):
|
|
53
|
+
|
|
54
|
+
if isinstance(ca_file_cache, str):
|
|
55
|
+
self.ca_file_cache = RootCACache(ca_file_cache)
|
|
56
|
+
else:
|
|
57
|
+
self.ca_file_cache = ca_file_cache
|
|
58
|
+
|
|
59
|
+
if isinstance(cert_cache, str):
|
|
60
|
+
self.cert_cache = FileCache(cert_cache)
|
|
61
|
+
elif isinstance(cert_cache, int):
|
|
62
|
+
self.cert_cache = LRUCache(max_size=cert_cache)
|
|
63
|
+
elif cert_cache is None:
|
|
64
|
+
self.cert_cache = LRUCache(max_size=100)
|
|
65
|
+
else:
|
|
66
|
+
self.cert_cache = cert_cache
|
|
67
|
+
|
|
68
|
+
self.ca_name = ca_name
|
|
69
|
+
|
|
70
|
+
self.cert_not_before = cert_not_before
|
|
71
|
+
self.cert_not_after = cert_not_after
|
|
72
|
+
|
|
73
|
+
res = self.load_root_ca_cert(overwrite=overwrite)
|
|
74
|
+
self.ca_cert, self.ca_key = res
|
|
75
|
+
|
|
76
|
+
def load_root_ca_cert(self, overwrite=False):
|
|
77
|
+
cert_str = None
|
|
78
|
+
|
|
79
|
+
if not overwrite:
|
|
80
|
+
cert_str = self.ca_file_cache.get(ROOT_CA)
|
|
81
|
+
|
|
82
|
+
# if cached, just read pem
|
|
83
|
+
if cert_str:
|
|
84
|
+
cert, key = self.read_pem(BytesIO(cert_str))
|
|
85
|
+
|
|
86
|
+
else:
|
|
87
|
+
cert, key = self.generate_ca_root(self.ca_name)
|
|
88
|
+
|
|
89
|
+
# Write cert + key
|
|
90
|
+
buff = BytesIO()
|
|
91
|
+
self.write_pem(buff, cert, key)
|
|
92
|
+
cert_str = buff.getvalue()
|
|
93
|
+
|
|
94
|
+
# store cert in cache
|
|
95
|
+
self.ca_file_cache[ROOT_CA] = cert_str
|
|
96
|
+
|
|
97
|
+
return cert, key
|
|
98
|
+
|
|
99
|
+
def is_host_ip(self, host):
|
|
100
|
+
try:
|
|
101
|
+
# if py2.7, need to decode to unicode str
|
|
102
|
+
if hasattr(host, 'decode'): #pragma: no cover
|
|
103
|
+
host = host.decode('ascii')
|
|
104
|
+
|
|
105
|
+
ipaddress.ip_address(host)
|
|
106
|
+
return True
|
|
107
|
+
except (ValueError, UnicodeDecodeError) as e:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
def get_wildcard_domain(self, host):
|
|
111
|
+
host_parts = host.split('.', 1)
|
|
112
|
+
if len(host_parts) < 2 or '.' not in host_parts[1]:
|
|
113
|
+
return host
|
|
114
|
+
|
|
115
|
+
ext = tldextract.extract(host)
|
|
116
|
+
|
|
117
|
+
# allow using parent domain if:
|
|
118
|
+
# 1) no suffix (unknown tld)
|
|
119
|
+
# 2) the parent domain contains 'domain.suffix', not just .suffix
|
|
120
|
+
if not ext.suffix or ext.domain + '.' + ext.suffix in host_parts[1]:
|
|
121
|
+
return host_parts[1]
|
|
122
|
+
|
|
123
|
+
return host
|
|
124
|
+
|
|
125
|
+
def load_cert(self, host, overwrite=False,
|
|
126
|
+
wildcard=False,
|
|
127
|
+
wildcard_use_parent=False,
|
|
128
|
+
include_cache_key=False):
|
|
129
|
+
|
|
130
|
+
is_ip = self.is_host_ip(host)
|
|
131
|
+
|
|
132
|
+
if is_ip:
|
|
133
|
+
wildcard = False
|
|
134
|
+
|
|
135
|
+
if wildcard and wildcard_use_parent:
|
|
136
|
+
host = self.get_wildcard_domain(host)
|
|
137
|
+
|
|
138
|
+
cert_str = None
|
|
139
|
+
|
|
140
|
+
if not overwrite:
|
|
141
|
+
cert_str = self.cert_cache.get(host)
|
|
142
|
+
|
|
143
|
+
# if cached, just read pem
|
|
144
|
+
if cert_str:
|
|
145
|
+
cert, key = self.read_pem(BytesIO(cert_str))
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
# if not cached, generate new root or host cert
|
|
149
|
+
cert, key = self.generate_host_cert(host,
|
|
150
|
+
self.ca_cert,
|
|
151
|
+
self.ca_key,
|
|
152
|
+
wildcard,
|
|
153
|
+
is_ip=is_ip)
|
|
154
|
+
|
|
155
|
+
# Write cert + key
|
|
156
|
+
buff = BytesIO()
|
|
157
|
+
self.write_pem(buff, cert, key)
|
|
158
|
+
cert_str = buff.getvalue()
|
|
159
|
+
|
|
160
|
+
# store cert in cache
|
|
161
|
+
self.cert_cache[host] = cert_str
|
|
162
|
+
|
|
163
|
+
if not include_cache_key:
|
|
164
|
+
return cert, key
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
cache_key = host
|
|
168
|
+
if hasattr(self.cert_cache, 'key_for_host'):
|
|
169
|
+
cache_key = self.cert_cache.key_for_host(host)
|
|
170
|
+
|
|
171
|
+
return cert, key, cache_key
|
|
172
|
+
|
|
173
|
+
def cert_for_host(self, host, overwrite=False,
|
|
174
|
+
wildcard=False):
|
|
175
|
+
|
|
176
|
+
res = self.load_cert(host, overwrite=overwrite,
|
|
177
|
+
wildcard=wildcard,
|
|
178
|
+
wildcard_use_parent=False,
|
|
179
|
+
include_cache_key=True)
|
|
180
|
+
|
|
181
|
+
return res[2]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_wildcard_cert(self, cert_host, overwrite=False):
|
|
186
|
+
res = self.load_cert(cert_host, overwrite=overwrite,
|
|
187
|
+
wildcard=True,
|
|
188
|
+
wildcard_use_parent=True,
|
|
189
|
+
include_cache_key=True)
|
|
190
|
+
|
|
191
|
+
return res[2]
|
|
192
|
+
|
|
193
|
+
def get_root_PKCS12(self):
|
|
194
|
+
p12 = crypto.PKCS12()
|
|
195
|
+
p12.set_certificate(self.ca_cert)
|
|
196
|
+
p12.set_privatekey(self.ca_key)
|
|
197
|
+
return p12.export()
|
|
198
|
+
|
|
199
|
+
def get_root_pem(self):
|
|
200
|
+
return self.ca_file_cache.get(ROOT_CA)
|
|
201
|
+
|
|
202
|
+
def get_root_pem_filename(self):
|
|
203
|
+
return self.ca_file_cache.ca_file
|
|
204
|
+
|
|
205
|
+
def _make_cert(self, certname):
|
|
206
|
+
cert = crypto.X509()
|
|
207
|
+
cert.set_serial_number(random.randint(0, 2 ** 64 - 1))
|
|
208
|
+
cert.get_subject().CN = certname
|
|
209
|
+
|
|
210
|
+
cert.set_version(2)
|
|
211
|
+
cert.gmtime_adj_notBefore(self.cert_not_before)
|
|
212
|
+
cert.gmtime_adj_notAfter(self.cert_not_after)
|
|
213
|
+
return cert
|
|
214
|
+
|
|
215
|
+
def generate_ca_root(self, ca_name, hash_func=DEF_HASH_FUNC):
|
|
216
|
+
# Generate key
|
|
217
|
+
key = crypto.PKey()
|
|
218
|
+
key.generate_key(crypto.TYPE_RSA, 2048)
|
|
219
|
+
|
|
220
|
+
# Generate cert
|
|
221
|
+
cert = self._make_cert(ca_name)
|
|
222
|
+
|
|
223
|
+
cert.set_issuer(cert.get_subject())
|
|
224
|
+
cert.set_pubkey(key)
|
|
225
|
+
cert.add_extensions([
|
|
226
|
+
crypto.X509Extension(b"basicConstraints",
|
|
227
|
+
True,
|
|
228
|
+
b"CA:TRUE, pathlen:0"),
|
|
229
|
+
|
|
230
|
+
crypto.X509Extension(b"keyUsage",
|
|
231
|
+
True,
|
|
232
|
+
b"keyCertSign, cRLSign"),
|
|
233
|
+
|
|
234
|
+
crypto.X509Extension(b"subjectKeyIdentifier",
|
|
235
|
+
False,
|
|
236
|
+
b"hash",
|
|
237
|
+
subject=cert),
|
|
238
|
+
])
|
|
239
|
+
cert.sign(key, hash_func)
|
|
240
|
+
|
|
241
|
+
return cert, key
|
|
242
|
+
|
|
243
|
+
def generate_host_cert(self, host, root_cert, root_key,
|
|
244
|
+
wildcard=False,
|
|
245
|
+
hash_func=DEF_HASH_FUNC,
|
|
246
|
+
is_ip=False):
|
|
247
|
+
|
|
248
|
+
host = host.encode('utf-8')
|
|
249
|
+
|
|
250
|
+
# Generate key
|
|
251
|
+
key = crypto.PKey()
|
|
252
|
+
key.generate_key(crypto.TYPE_RSA, 2048)
|
|
253
|
+
|
|
254
|
+
# Generate CSR
|
|
255
|
+
req = crypto.X509Req()
|
|
256
|
+
req.get_subject().CN = host
|
|
257
|
+
req.set_pubkey(key)
|
|
258
|
+
req.sign(key, hash_func)
|
|
259
|
+
|
|
260
|
+
# Generate Cert
|
|
261
|
+
cert = self._make_cert(host)
|
|
262
|
+
|
|
263
|
+
cert.set_issuer(root_cert.get_subject())
|
|
264
|
+
cert.set_pubkey(req.get_pubkey())
|
|
265
|
+
|
|
266
|
+
primary = b'DNS:' + host
|
|
267
|
+
|
|
268
|
+
if wildcard:
|
|
269
|
+
alt_hosts = primary + b', DNS:*.' + host
|
|
270
|
+
|
|
271
|
+
elif is_ip:
|
|
272
|
+
alt_hosts = b'IP:' + host + b', ' + primary
|
|
273
|
+
|
|
274
|
+
else:
|
|
275
|
+
alt_hosts = primary
|
|
276
|
+
|
|
277
|
+
cert.add_extensions([
|
|
278
|
+
crypto.X509Extension(b'subjectAltName',
|
|
279
|
+
False,
|
|
280
|
+
alt_hosts)])
|
|
281
|
+
|
|
282
|
+
cert.sign(root_key, hash_func)
|
|
283
|
+
return cert, key
|
|
284
|
+
|
|
285
|
+
def write_pem(self, buff, cert, key):
|
|
286
|
+
buff.write(crypto.dump_privatekey(FILETYPE_PEM, key))
|
|
287
|
+
buff.write(crypto.dump_certificate(FILETYPE_PEM, cert))
|
|
288
|
+
|
|
289
|
+
def read_pem(self, buff):
|
|
290
|
+
cert = crypto.load_certificate(FILETYPE_PEM, buff.read())
|
|
291
|
+
buff.seek(0)
|
|
292
|
+
key = crypto.load_privatekey(FILETYPE_PEM, buff.read())
|
|
293
|
+
return cert, key
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# =================================================================
|
|
297
|
+
class FileCache(object):
|
|
298
|
+
def __init__(self, certs_dir):
|
|
299
|
+
self._lock = threading.Lock()
|
|
300
|
+
self.certs_dir = certs_dir
|
|
301
|
+
self.modified = False
|
|
302
|
+
|
|
303
|
+
if self.certs_dir and not os.path.exists(self.certs_dir):
|
|
304
|
+
os.makedirs(self.certs_dir)
|
|
305
|
+
|
|
306
|
+
def key_for_host(self, host):
|
|
307
|
+
host = host.replace(':', '-')
|
|
308
|
+
return os.path.join(self.certs_dir, host) + '.pem'
|
|
309
|
+
|
|
310
|
+
def __setitem__(self, host, cert_string):
|
|
311
|
+
filename = self.key_for_host(host)
|
|
312
|
+
with self._lock:
|
|
313
|
+
with open(filename, 'wb') as fh:
|
|
314
|
+
fh.write(cert_string)
|
|
315
|
+
self.modified = True
|
|
316
|
+
|
|
317
|
+
def get(self, host):
|
|
318
|
+
filename = self.key_for_host(host)
|
|
319
|
+
try:
|
|
320
|
+
with open(filename, 'rb') as fh:
|
|
321
|
+
return fh.read()
|
|
322
|
+
except:
|
|
323
|
+
return b''
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# =================================================================
|
|
327
|
+
class RootCACache(FileCache):
|
|
328
|
+
def __init__(self, ca_file):
|
|
329
|
+
self.ca_file = ca_file
|
|
330
|
+
ca_dir = os.path.dirname(ca_file) or '.'
|
|
331
|
+
super(RootCACache, self).__init__(ca_dir)
|
|
332
|
+
|
|
333
|
+
def key_for_host(self, host=None):
|
|
334
|
+
return self.ca_file
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# =================================================================
|
|
338
|
+
class LRUCache(OrderedDict):
|
|
339
|
+
def __init__(self, max_size):
|
|
340
|
+
super(LRUCache, self).__init__()
|
|
341
|
+
self.max_size = max_size
|
|
342
|
+
|
|
343
|
+
def __setitem__(self, host, cert_string):
|
|
344
|
+
super(LRUCache, self).__setitem__(host, cert_string)
|
|
345
|
+
if len(self) > self.max_size:
|
|
346
|
+
self.popitem(last=False)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# =================================================================
|
|
350
|
+
def main(args=None):
|
|
351
|
+
parser = ArgumentParser(description='Certificate Authority Cert Maker Tools')
|
|
352
|
+
|
|
353
|
+
parser.add_argument('root_ca_cert',
|
|
354
|
+
help='Path to existing or new root CA file')
|
|
355
|
+
|
|
356
|
+
parser.add_argument('-c', '--certname', action='store', default=CERT_NAME,
|
|
357
|
+
help='Name for root certificate')
|
|
358
|
+
|
|
359
|
+
parser.add_argument('-n', '--hostname',
|
|
360
|
+
help='Hostname certificate to create')
|
|
361
|
+
|
|
362
|
+
parser.add_argument('-d', '--certs-dir', default=CERTS_DIR,
|
|
363
|
+
help='Directory for host certificates')
|
|
364
|
+
|
|
365
|
+
parser.add_argument('-f', '--force', action='store_true',
|
|
366
|
+
help='Overwrite certificates if they already exist')
|
|
367
|
+
|
|
368
|
+
parser.add_argument('-w', '--wildcard_cert', action='store_true',
|
|
369
|
+
help='add wildcard SAN to host: *.<host>, <host>')
|
|
370
|
+
|
|
371
|
+
r = parser.parse_args(args=args)
|
|
372
|
+
|
|
373
|
+
certs_dir = r.certs_dir
|
|
374
|
+
wildcard = r.wildcard_cert
|
|
375
|
+
|
|
376
|
+
root_cert = r.root_ca_cert
|
|
377
|
+
hostname = r.hostname
|
|
378
|
+
|
|
379
|
+
if not hostname:
|
|
380
|
+
overwrite = r.force
|
|
381
|
+
else:
|
|
382
|
+
overwrite = False
|
|
383
|
+
|
|
384
|
+
cert_cache = FileCache(r.certs_dir)
|
|
385
|
+
ca_file_cache = RootCACache(root_cert)
|
|
386
|
+
|
|
387
|
+
ca = CertificateAuthority(ca_name=r.certname,
|
|
388
|
+
ca_file_cache=ca_file_cache,
|
|
389
|
+
cert_cache=cert_cache,
|
|
390
|
+
overwrite=overwrite)
|
|
391
|
+
|
|
392
|
+
# Just creating the root cert
|
|
393
|
+
if not hostname:
|
|
394
|
+
if ca_file_cache.modified:
|
|
395
|
+
print('Created new root cert: "' + root_cert + '"')
|
|
396
|
+
return 0
|
|
397
|
+
else:
|
|
398
|
+
print('Root cert "' + root_cert +
|
|
399
|
+
'" already exists,' + ' use -f to overwrite')
|
|
400
|
+
return 1
|
|
401
|
+
|
|
402
|
+
# Sign a certificate for a given host
|
|
403
|
+
overwrite = r.force
|
|
404
|
+
ca.load_cert(hostname, overwrite=overwrite,
|
|
405
|
+
wildcard=wildcard,
|
|
406
|
+
wildcard_use_parent=False)
|
|
407
|
+
|
|
408
|
+
if cert_cache.modified:
|
|
409
|
+
print('Created new cert "' + hostname +
|
|
410
|
+
'" signed by root cert ' +
|
|
411
|
+
root_cert)
|
|
412
|
+
return 0
|
|
413
|
+
|
|
414
|
+
else:
|
|
415
|
+
print('Cert for "' + hostname + '" already exists,' +
|
|
416
|
+
' use -f to overwrite')
|
|
417
|
+
return 1
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if __name__ == "__main__": #pragma: no cover
|
|
421
|
+
main()
|
|
File without changes
|