зеркало из https://github.com/mozilla/tls-canary.git
Turning TLS Cananry into a proper Python package
* Moving canary files into module directory * Fixing imports * Adding distribution package information * Adding PyPI support * Overhauling documentation * Fixing resources access in tests and sources_db * Adding pep8 git pre-commit hook * Removing obsolete and dysfunct 'google' host DB * Version bump to 3.1.0a14
This commit is contained in:
Родитель
d8a0dc44bb
Коммит
84b0526546
|
@ -1,15 +1,20 @@
|
|||
.Python
|
||||
bin
|
||||
lib
|
||||
share
|
||||
man
|
||||
include
|
||||
Scripts
|
||||
tcl
|
||||
venv
|
||||
bin/
|
||||
lib/
|
||||
share/
|
||||
man/
|
||||
include/
|
||||
Scripts/
|
||||
tcl/
|
||||
venv/
|
||||
venv3/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.un~
|
||||
*.py~
|
||||
.idea
|
||||
.idea/
|
||||
pip-selfcheck.json
|
||||
*.egg-info
|
||||
*.egg-info/
|
||||
dist/
|
||||
.eggs/
|
||||
doc/
|
|
@ -0,0 +1,374 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
global-exclude *.py[co]
|
||||
include LICENSE.txt
|
||||
include README.rst
|
||||
recursive-include tlscanary/default_profile *
|
||||
recursive-include tlscanary/js *
|
||||
recursive-include tlscanary/sources *.csv
|
||||
recursive-include tlscanary/template *
|
||||
exclude README.md
|
||||
exclude tests/*
|
145
README.md
145
README.md
|
@ -1,68 +1,142 @@
|
|||
# TLS Canary version 3
|
||||
Automated testing of Firefox for TLS/SSL web compatibility
|
||||
# TLS Canary
|
||||
|
||||
Regression scanning results live here:
|
||||
http://tlscanary.mozilla.org
|
||||
[![PyPI Package version](https://badge.fury.io/py/tlscanary.svg)](https://pypi.python.org/pypi/tlscanary)
|
||||
|
||||
TLS Canary is a [TLS/SSL](https://en.wikipedia.org/wiki/Transport_Layer_Security) testing framework for the
|
||||
[Mozilla Firefox](https://www.mozilla.org/firefox) web browser. It is used by developers to run regression and
|
||||
performance tests against a large number of live HTTPS-enabled hosts on the Internet.
|
||||
|
||||
Results of the regression scans are published in HTML format here:
|
||||
* http://tlscanary.mozilla.org
|
||||
|
||||
## This project
|
||||
* Downloads a branch build and a release build of Firefox.
|
||||
* Downloads a test build and a base build of Firefox for comparison.
|
||||
* Automatically runs thousands of secure sites on those builds.
|
||||
* Diffs the results and presents potential regressions in an HTML page for further diagnosis.
|
||||
* Does performance regression testing
|
||||
* Extracts SSL state information
|
||||
* Can maintain an updated list of TLS-enabled top sites
|
||||
* Does performance regression testing.
|
||||
* Extracts SSL state information.
|
||||
* Can maintain an updated list of TLS-enabled top sites.
|
||||
* Requires a highly reliable network link. **WiFi will not do.**
|
||||
|
||||
## Requirements
|
||||
* Python 2.7
|
||||
* virtualenv (highly recommended)
|
||||
* 7zip
|
||||
* git
|
||||
* Go compiler
|
||||
* OpenSSL-dev
|
||||
* libffi-dev
|
||||
|
||||
The script [linux_bootstrap.sh](linux_bootstrap.sh) provides bootstrapping for an Ubuntu-based EC2 instance.
|
||||
### Dependencies for Debian/Ubuntu users
|
||||
Assuming that you want to run TLS Canary on a regular graphical desktop machine, these are the packages that
|
||||
you require:
|
||||
```
|
||||
sudo apt-get install python python-dev gcc golang-go p7zip-full libssl-dev libffi-dev
|
||||
```
|
||||
|
||||
## Linux and Mac usage
|
||||
The script [linux_bootstrap.sh](bootstrap/linux_bootstrap.sh) provides bootstrapping for a headless Ubuntu-based EC2
|
||||
instance. This requires the installation of a few GUI libraries required to run Firefox that are already available
|
||||
on a regular desktop machine. The script may or may not work for your other favourite Debian-based distribution.
|
||||
|
||||
### Dependencies for Mac users
|
||||
Assuming that your're using [Homebrew](https://brew.sh/) for package management, this should set you up:
|
||||
```
|
||||
brew install python p7zip go openssl libffi
|
||||
```
|
||||
|
||||
### Dependencies for Windows users
|
||||
Windows support targets **PowerShell 5.1** on **Windows 10**. Windows 7 and 8 are generally able to run TLS Canary,
|
||||
but expect minor unicode encoding issues in terminal logging output.
|
||||
|
||||
First, [install Chocolatey](https://chocolatey.org/install), then run the following command in an admin PowerShell
|
||||
to install the dependencies:
|
||||
```
|
||||
choco install 7zip.commandline git golang openssh python2
|
||||
```
|
||||
|
||||
## For end users
|
||||
TLS Canary can be installed directly as a stable package from PyPI and as experimental package directly from GitHub.
|
||||
The following command will install the latest stable release of TLS Canary to your current Python environment:
|
||||
```
|
||||
pip install --upgrade tlscanary
|
||||
```
|
||||
|
||||
If you require the bleeding-edge developer version with the latest features and added instability, you can run
|
||||
```
|
||||
pip install --upgrade git+git://github.com/mozilla/tls-canary.git
|
||||
```
|
||||
|
||||
After that the `tlscanary` binary will be available in your Python environment:
|
||||
```
|
||||
tlscanary --help
|
||||
```
|
||||
|
||||
## Usage examples
|
||||
```bash
|
||||
# Run a quick regression test against the first 50000 hosts in the default `top` database
|
||||
tlscanary -r /tmp/reports -l 50000
|
||||
|
||||
# Compile a fresh 'top 1000' host database called `mini`
|
||||
tlscanary -s mini -l 1000 -x1 srcupdate
|
||||
|
||||
# Show a list of available host databases
|
||||
tlscanary -s list
|
||||
|
||||
# Use your fresh `mini` database for a quick regession test and see lots of things happening
|
||||
tlscanary -s mini -r /tmp/report --debug
|
||||
```
|
||||
|
||||
Please refer to the complete argument and mode references below.
|
||||
|
||||
## For developers
|
||||
For development you will additionally need to install:
|
||||
|
||||
* git
|
||||
* virtualenv (highly recommended)
|
||||
|
||||
*git* can be installed with your favourite package manager. *virtualenv* comes with a simple `pip install virtualenv`.
|
||||
|
||||
### Developing on Linux or Mac
|
||||
These are the commands that should set you up for development work:
|
||||
```
|
||||
git clone https://github.com/mozilla/tls-canary
|
||||
cd tls-canary
|
||||
virtualenv .
|
||||
source bin/activate
|
||||
pip install -e .
|
||||
tls_canary --help
|
||||
tls_canary --reportdir=/tmp/test --debug debug
|
||||
virtualenv -p python2.7 venv
|
||||
source venv/bin/activate
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
## Windows support
|
||||
Windows support targets **PowerShell 5.1** on **Windows 10**. Windows 7 and 8
|
||||
are generally able to run TLS Canary, but expect minor unicode
|
||||
encoding issues in terminal logging output.
|
||||
The latter command should be used regularly to install new Python dependencies that a pulled update may require.
|
||||
|
||||
### Run in an admin PowerShell
|
||||
First, [install Chocolatey](https://chocolatey.org/install), then
|
||||
```
|
||||
choco install 7zip.commandline git golang openssh python2
|
||||
choco install python3 # Optional, provides the virtualenv cmdlet
|
||||
pip install virtualenv # Not required if python3 installed
|
||||
```
|
||||
|
||||
### Run in a user PowerShell
|
||||
### Developing on Windows
|
||||
Developing TLS Canary on Windows is not something we practice regularly. If you encounter quirks along the way,
|
||||
please do not hesitate to open an issue here on GitHub. The following commands, executed in a PowerShell session
|
||||
with user privileges, should set you up for development:
|
||||
```
|
||||
git clone https://github.com/mozilla/tls-canary
|
||||
cd tls-canary
|
||||
virtualenv -p c:\python27\python.exe venv
|
||||
venv\Scripts\activate
|
||||
pip install -e .
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
### Note for developers
|
||||
There's a pre-commit hook for git that you can use for checking for PEP8 violations. You can install it
|
||||
by running
|
||||
### Running tests
|
||||
There are two ways to run the test suite:
|
||||
```
|
||||
python setup.py test
|
||||
nosetests -sv
|
||||
```
|
||||
|
||||
They are largely equivalent, but the former takes care of missing test dependencies, while running `nosetests`
|
||||
directly offers more control.
|
||||
|
||||
### Installing the pre-commit hook for git
|
||||
There's a pre-commit hook for git that you can use for automated checking for
|
||||
[PEP 8](https://www.python.org/dev/peps/pep-0008/) violations. You can install it by running
|
||||
```
|
||||
ln -sf ../../hooks/pre-commit .git/hooks/
|
||||
```
|
||||
in the top-level project directory. By using a symbolic link, you will automatically get updates once the hook
|
||||
in the repo changes. This is highly recommended. You can also copy the script manually, but then you have to
|
||||
tkae care of updates yourself.
|
||||
|
||||
### Command line arguments
|
||||
Argument | Choices / **default** | Description
|
||||
|
@ -94,6 +168,3 @@ performance | Runs a performance analysis against the hosts in the test set. Use
|
|||
regression | Runs a TLS regression test, comparing the 'test' candidate against the 'baseline' candidate. Only reports errors that are new to the test candiate. No error generated by baseline can make it to the report.
|
||||
scan | This mode only collects connection state information for every host in the test set.
|
||||
srcupdate | Compile a fresh set of TLS-enabled 'top' sites from the *Umbrella Top 1M* list. Use `-l` to override the default target size of 500k hosts. Use `-x` to adjust the number of passes for errors. Use `-x1` for a factor two speed improvement with slightly less stable results. Use `-b` to change the Firefox version used for filtering. You can use `-s` to create a new database, but you can't make it the default.
|
||||
|
||||
## Testing
|
||||
* nosetests -sv
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
TLS Canary
|
||||
==========
|
||||
|
||||
`TLS Canary <https://github.com/mozilla/tls-canary>`_ is a
|
||||
`TLS/SSL <https://en.wikipedia.org/wiki/Transport_Layer_Security>`_ testing framework for the
|
||||
`Mozilla Firefox <https://www.mozilla.org/firefox>`_ web browser. It is used by developers to run
|
||||
regression and performance tests against a large number of HTTPS-enabled hosts on the Internet.
|
||||
|
||||
--------
|
||||
|
||||
The TLS Canary Python package has several non-Python dependencies that need to be satisfied for the script to run.
|
||||
For installation instructions, usage information, and issue tracking, please visit the `Project Page
|
||||
<https://github.com/mozilla/tls-canary>`_ on GitHub.
|
|
@ -1,7 +0,0 @@
|
|||
coloredlogs
|
||||
cryptography
|
||||
ipython
|
||||
nose >= 1.3
|
||||
mock
|
||||
pep8
|
||||
worq
|
|
@ -0,0 +1,2 @@
|
|||
[metadata]
|
||||
description-file = README.rst
|
78
setup.py
78
setup.py
|
@ -4,26 +4,64 @@
|
|||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
PACKAGE_VERSION = '3.1.0-alpha.3'
|
||||
PACKAGE_NAME = 'tlscanary'
|
||||
PACKAGE_VERSION = '3.1.0a14'
|
||||
|
||||
# Dependencies
|
||||
with open('requirements.txt') as f:
|
||||
deps = f.read().splitlines()
|
||||
INSTALL_REQUIRES = [
|
||||
'coloredlogs',
|
||||
'cryptography',
|
||||
'ipython',
|
||||
'worq'
|
||||
]
|
||||
|
||||
setup(name='tls_canary',
|
||||
version=PACKAGE_VERSION,
|
||||
description='TLS/SSL Test Suite for Firefox',
|
||||
classifiers=[],
|
||||
keywords='mozilla',
|
||||
author='Christiane Ruetten',
|
||||
author_email='cr@mozilla.com',
|
||||
url='https://github.com/cr/tls-canary',
|
||||
license='MPL',
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=deps,
|
||||
entry_points={"console_scripts": [
|
||||
"tls_canary = main:main"
|
||||
]}
|
||||
TESTS_REQUIRE = [
|
||||
'nose',
|
||||
'mock'
|
||||
]
|
||||
|
||||
DEV_REQUIRES = [
|
||||
'nose',
|
||||
'mock',
|
||||
'pep8'
|
||||
]
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
version=PACKAGE_VERSION,
|
||||
description='TLS/SSL Test Suite for Mozilla Firefox',
|
||||
classifiers=[
|
||||
'Environment :: Console',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||
'Natural Language :: English',
|
||||
'Operating System :: MacOS :: MacOS X',
|
||||
'Operating System :: Microsoft :: Windows :: Windows 10',
|
||||
'Operating System :: Microsoft :: Windows :: Windows 7',
|
||||
'Operating System :: Microsoft :: Windows :: Windows 8',
|
||||
'Operating System :: Microsoft :: Windows :: Windows 8.1',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Software Development :: Quality Assurance',
|
||||
'Topic :: Software Development :: Testing'
|
||||
],
|
||||
keywords=['mozilla', 'firefox', 'tls', 'regression-testing', 'testing'],
|
||||
author='Christiane Ruetten',
|
||||
author_email='cr@mozilla.com',
|
||||
url='https://github.com/mozilla/tls-canary',
|
||||
download_url='https://github.com/mozilla/tls-canary/archive/latest.tar.gz',
|
||||
license='MPL2',
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
include_package_data=True, # See MANIFEST.in
|
||||
zip_safe=False,
|
||||
use_2to3=False,
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
tests_require=TESTS_REQUIRE,
|
||||
extras_require={'dev': DEV_REQUIRES}, # For `pip install -e .[dev]`
|
||||
test_suite='nose.collector',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'tlscanary = tlscanary.main:main'
|
||||
]
|
||||
}
|
||||
)
|
||||
|
|
Двоичные данные
sources/google_ct_list.csv.bz2
Двоичные данные
sources/google_ct_list.csv.bz2
Двоичный файл не отображается.
|
@ -2,12 +2,11 @@
|
|||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import firefox_downloader as fd
|
||||
import firefox_extractor as fe
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tlscanary.firefox_extractor as fe
|
||||
|
||||
|
||||
# Global variables for all tests
|
||||
|
@ -16,7 +15,6 @@ import firefox_extractor as fe
|
|||
# the import happens before setup is run.
|
||||
test_app = None
|
||||
test_archive = None
|
||||
test_dir = os.path.split(__file__)[0]
|
||||
tmp_dir = None
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from nose.tools import *
|
|||
import os
|
||||
from time import sleep, time
|
||||
|
||||
import cache
|
||||
import tlscanary.cache as cache
|
||||
import tests
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ from nose.tools import *
|
|||
import os
|
||||
from time import sleep
|
||||
|
||||
import firefox_downloader as fd
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tests
|
||||
|
||||
|
||||
|
@ -45,6 +45,7 @@ def test_firefox_downloader_exceptions():
|
|||
@mock.patch('sys.stdout') # to silence progress bar
|
||||
def test_firefox_downloader_downloading(mock_stdout, mock_urlopen):
|
||||
"""Test the download function"""
|
||||
del mock_stdout
|
||||
|
||||
# This test is checking caching behavior, hence:
|
||||
# Using a test-specific test directory to not wipe regular cache.
|
||||
|
@ -55,14 +56,14 @@ def test_firefox_downloader_downloading(mock_stdout, mock_urlopen):
|
|||
mock_req = mock.Mock()
|
||||
mock_read = mock.Mock(side_effect=("foo", "bar", None))
|
||||
mock_info = mock.Mock()
|
||||
mock_getheader = mock.Mock(return_value="6")
|
||||
mock_info.return_value = mock.Mock(getheader=mock_getheader)
|
||||
mock_get = mock.Mock(return_value="6")
|
||||
mock_info.return_value = mock.Mock(get=mock_get)
|
||||
mock_req.info = mock_info
|
||||
mock_req.read = mock_read
|
||||
mock_urlopen.return_value = mock_req
|
||||
|
||||
output_file_name = fdl.download("nightly", "linux", use_cache=True)
|
||||
assert_equal(mock_getheader.call_args_list, [(("Content-Length",),)],
|
||||
assert_equal(mock_get.call_args_list, [(("Content-Length",),)],
|
||||
"only checks content length (assumed by test mock)")
|
||||
expected_url = """https://download.mozilla.org/?product=firefox-nightly-latest&os=linux64&lang=en-US"""
|
||||
assert_true(mock_urlopen.call_args_list == [((expected_url,),)], "downloads the expected URL")
|
||||
|
@ -90,8 +91,8 @@ def test_firefox_downloader_downloading(mock_stdout, mock_urlopen):
|
|||
assert_true(mock_read.called, "re-downloads when cache is stale")
|
||||
|
||||
# Test caching when file changes upstream (checks file size).
|
||||
mock_getheader.reset_mock()
|
||||
mock_getheader.return_value = "7"
|
||||
mock_get.reset_mock()
|
||||
mock_get.return_value = "7"
|
||||
mock_read.reset_mock()
|
||||
mock_read.side_effect = ("foo", "barr", None)
|
||||
fdl.download("nightly", "linux", use_cache=True)
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
from nose import SkipTest
|
||||
from nose.tools import *
|
||||
import os
|
||||
import pkg_resources as pkgr
|
||||
import subprocess
|
||||
|
||||
|
||||
import firefox_extractor as fe
|
||||
import firefox_app as fa
|
||||
import tlscanary.firefox_extractor as fe
|
||||
import tlscanary.firefox_app as fa
|
||||
import tests
|
||||
|
||||
|
||||
|
@ -22,7 +23,7 @@ def test_osx_extractor():
|
|||
if sz_version < 16:
|
||||
raise SkipTest('7-zip version 16 required to extract DMG images')
|
||||
|
||||
test_archive = os.path.join(tests.test_dir, "files", "firefox-nightly_osx-dummy.dmg")
|
||||
test_archive = pkgr.resource_filename(__name__, "files/firefox-nightly_osx-dummy.dmg")
|
||||
assert_true(os.path.isfile(test_archive))
|
||||
|
||||
app = fe.extract(test_archive, tests.tmp_dir)
|
||||
|
@ -41,7 +42,7 @@ def test_osx_extractor():
|
|||
def test_linux_extractor():
|
||||
"""Extractor can extract a Linux Nightly archive"""
|
||||
|
||||
test_archive = os.path.join(tests.test_dir, "files", "firefox-nightly_linux-dummy.tar.bz2")
|
||||
test_archive = pkgr.resource_filename(__name__, "files/firefox-nightly_linux-dummy.tar.bz2")
|
||||
assert_true(os.path.isfile(test_archive))
|
||||
|
||||
app = fe.extract(test_archive, tests.tmp_dir)
|
||||
|
@ -60,7 +61,7 @@ def test_linux_extractor():
|
|||
def test_win_extractor():
|
||||
"""Extractor can extract a Windows Nightly archive"""
|
||||
|
||||
test_archive = os.path.join(tests.test_dir, "files", "firefox-nightly_win-dummy.exe")
|
||||
test_archive = pkgr.resource_filename(__name__, "files/firefox-nightly_win-dummy.exe")
|
||||
assert_true(os.path.isfile(test_archive))
|
||||
|
||||
app = fe.extract(test_archive, tests.tmp_dir)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
from nose.tools import *
|
||||
import os
|
||||
|
||||
import sources_db as sdb
|
||||
import tlscanary.sources_db as sdb
|
||||
import tests
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ from nose.tools import *
|
|||
from time import sleep
|
||||
|
||||
import tests
|
||||
import xpcshell_worker as xw
|
||||
import tlscanary.xpcshell_worker as xw
|
||||
|
||||
|
||||
@mock.patch('sys.stdout') # to silence progress bar
|
||||
|
|
|
@ -20,7 +20,7 @@ def get_to_file(url, filename):
|
|||
try:
|
||||
# TODO: Validate the server's SSL certificate
|
||||
req = urllib2.urlopen(url)
|
||||
file_size = int(req.info().getheader('Content-Length').strip())
|
||||
file_size = int(req.info().get('Content-Length').strip())
|
||||
|
||||
# Caching logic is: don't re-download if file of same size is
|
||||
# already in place. TODO: Switch to ETag if that's not good enough.
|
|
@ -29,7 +29,7 @@ def get_argparser():
|
|||
Argument parsing
|
||||
:return: Parsed arguments object
|
||||
"""
|
||||
pkg_version = pkg_resources.require("tls_canary")[0].version
|
||||
pkg_version = pkg_resources.require("tlscanary")[0].version
|
||||
home = os.path.expanduser('~')
|
||||
# By nature of workdir being undetermined at this point, user-defined test sets in
|
||||
# the override directory can not override the default test set. The defaulting logic
|
||||
|
@ -38,7 +38,7 @@ def get_argparser():
|
|||
testset_default = src.default
|
||||
release_choice, _, test_default, base_default = fd.FirefoxDownloader.list()
|
||||
|
||||
parser = argparse.ArgumentParser(prog="tls_canary")
|
||||
parser = argparse.ArgumentParser(prog="tlscanary")
|
||||
parser.add_argument('--version', action='version', version='%(prog)s ' + pkg_version)
|
||||
parser.add_argument('-b', '--base',
|
||||
help='Firefox base version to compare against (default: `%s`)' % base_default,
|
|
@ -10,11 +10,11 @@ import shutil
|
|||
import stat
|
||||
import sys
|
||||
|
||||
import firefox_downloader as fd
|
||||
import firefox_extractor as fe
|
||||
import one_crl_downloader as one_crl
|
||||
import worker_pool as wp
|
||||
import xpcshell_worker as xw
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tlscanary.firefox_extractor as fe
|
||||
import tlscanary.one_crl_downloader as one_crl
|
||||
import tlscanary.worker_pool as wp
|
||||
import tlscanary.xpcshell_worker as xw
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
|
@ -6,9 +6,9 @@ import datetime
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from modes.regression import RegressionMode
|
||||
import firefox_downloader as fd
|
||||
import report
|
||||
from regression import RegressionMode
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tlscanary.report as report
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
|
@ -9,10 +9,10 @@ import os
|
|||
import pkg_resources as pkgr
|
||||
import sys
|
||||
|
||||
from modes.basemode import BaseMode
|
||||
import firefox_downloader as fd
|
||||
import report
|
||||
import sources_db as sdb
|
||||
from basemode import BaseMode
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tlscanary.report as report
|
||||
import tlscanary.sources_db as sdb
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -56,10 +56,7 @@ class RegressionMode(BaseMode):
|
|||
# Code paths after this will generate a report, so check
|
||||
# whether the report dir is a valid target. Specifically, prevent
|
||||
# writing to the module directory.
|
||||
module_dir = pkgr.require("tls_canary")[0].location
|
||||
|
||||
logging.info ("module dir: %s", module_dir)
|
||||
logging.info ("report dir: %s", os.path.normcase(os.path.realpath(self.args.reportdir)))
|
||||
module_dir = pkgr.require("tlscanary")[0].location
|
||||
if os.path.normcase(os.path.realpath(self.args.reportdir))\
|
||||
.startswith(os.path.normcase(os.path.realpath(module_dir))):
|
||||
logger.critical("Refusing to write report to module directory. Please set --reportdir")
|
|
@ -8,10 +8,10 @@ import os
|
|||
import pkg_resources as pkgr
|
||||
import sys
|
||||
|
||||
from modes.basemode import BaseMode
|
||||
import firefox_downloader as fd
|
||||
import report
|
||||
import sources_db as sdb
|
||||
from basemode import BaseMode
|
||||
import tlscanary.firefox_downloader as fd
|
||||
import tlscanary.report as report
|
||||
import tlscanary.sources_db as sdb
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -47,7 +47,7 @@ class ScanMode(BaseMode):
|
|||
# Code paths after this will generate a report, so check
|
||||
# whether the report dir is a valid target. Specifically, prevent
|
||||
# writing to the module directory.
|
||||
module_dir = pkgr.require("tls_canary")[0].location
|
||||
module_dir = pkgr.require("tlscanary")[0].location
|
||||
if os.path.normcase(os.path.realpath(self.args.reportdir))\
|
||||
.startswith(os.path.normcase(os.path.realpath(module_dir))):
|
||||
logger.critical("Refusing to write report to module directory. Please set --reportdir")
|
|
@ -9,9 +9,9 @@ import os
|
|||
import sys
|
||||
import zipfile
|
||||
|
||||
from firefox_downloader import get_to_file
|
||||
from modes.basemode import BaseMode
|
||||
import sources_db as sdb
|
||||
from basemode import BaseMode
|
||||
from tlscanary.firefox_downloader import get_to_file
|
||||
import tlscanary.sources_db as sdb
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
Не удается отобразить этот файл, потому что он слишком большой.
|
|
@ -5,20 +5,20 @@
|
|||
import csv
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources as pkgr
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
module_dir = os.path.abspath(os.path.split(__file__)[0])
|
||||
module_data_dir = os.path.join(module_dir, "sources")
|
||||
|
||||
|
||||
def list_sources(data_dirs):
|
||||
def list_sources(override_dir=None):
|
||||
"""
|
||||
This function trawls through all the sources CSV files in the given data directories
|
||||
and generates a dictionary of handle names and associated file names. Per default, the
|
||||
base part of the file name (without `.csv`) is used as handle for that list.
|
||||
This function trawls through all the sources CSV files in the module CSV directory and
|
||||
the given override directory, and generates a dictionary of database handle names and
|
||||
associated file names. Per default, the base part of the file name (without `.csv`) is
|
||||
used as handle for that list.
|
||||
|
||||
Files in latter data directories override files in former ones.
|
||||
Files in the override directory override files in the module directory.
|
||||
|
||||
If the first line of a CSV file begins with a `#`, it is interpreted as a
|
||||
colon-separated list of keywords. If it contains the keyword `handle`, the last
|
||||
|
@ -28,7 +28,7 @@ def list_sources(data_dirs):
|
|||
When multiple CSV files use the `default` keyword, the lexicographically last file
|
||||
name is used as default.
|
||||
|
||||
:param data_dirs: List of paths to directories containing CSV files
|
||||
:param override_dir: str of directory used for overrides
|
||||
:return: (dict mapping handles to file names, str handle of default list)
|
||||
"""
|
||||
global logger
|
||||
|
@ -36,18 +36,25 @@ def list_sources(data_dirs):
|
|||
sources_list = {}
|
||||
default_source = None
|
||||
|
||||
for data_dir in data_dirs:
|
||||
if not os.path.isdir(data_dir):
|
||||
continue
|
||||
for root, dirs, files in os.walk(data_dir):
|
||||
# First, look for CSV files in module resources
|
||||
csv_files = [os.path.abspath(pkgr.resource_filename(__name__, "sources/%s" % name))
|
||||
for name in pkgr.resource_listdir(__name__, "sources")
|
||||
if name.endswith(".csv")]
|
||||
|
||||
# Then look for CSV files in the override directory
|
||||
if override_dir is not None and os.path.isdir(override_dir):
|
||||
for root, dirs, files in os.walk(override_dir):
|
||||
for name in files:
|
||||
if name.endswith(".csv"):
|
||||
file_name = os.path.abspath(os.path.join(root, name))
|
||||
logger.debug("Indexing sources database file `%s`" % file_name)
|
||||
source_handle, is_default = parse_csv_header(file_name)
|
||||
sources_list[source_handle] = os.path.abspath(os.path.join(root, name))
|
||||
if is_default:
|
||||
default_source = source_handle
|
||||
csv_files.append(os.path.abspath(os.path.join(root, name)))
|
||||
|
||||
# Finally extract metadata from files and compile sources list
|
||||
for file_name in csv_files:
|
||||
logger.debug("Indexing database resource `%s`" % file_name)
|
||||
source_handle, is_default = parse_csv_header(file_name)
|
||||
sources_list[source_handle] = file_name
|
||||
if is_default:
|
||||
default_source = source_handle
|
||||
|
||||
return sources_list, default_source
|
||||
|
||||
|
@ -69,12 +76,12 @@ def parse_csv_header(file_name):
|
|||
is_default = False
|
||||
with open(file_name) as f:
|
||||
line = f.readline().strip()
|
||||
if line.startswith("#"):
|
||||
keywords = line.lstrip("#").split(":")
|
||||
if "handle" in keywords:
|
||||
source_handle = keywords[-1]
|
||||
if "default" in keywords:
|
||||
is_default = True
|
||||
if line.startswith("#"):
|
||||
keywords = line.lstrip("#").split(":")
|
||||
if "handle" in keywords:
|
||||
source_handle = keywords[-1]
|
||||
if "default" in keywords:
|
||||
is_default = True
|
||||
return source_handle, is_default
|
||||
|
||||
|
||||
|
@ -94,13 +101,12 @@ class SourcesDB(object):
|
|||
`hostname`, and optionally the column `rank`.
|
||||
"""
|
||||
def __init__(self, args=None):
|
||||
global module_data_dir
|
||||
self.__args = args
|
||||
if args is not None:
|
||||
self.__data_dirs = [module_data_dir, os.path.join(args.workdir, "sources")]
|
||||
self.__override_dir = os.path.join(args.workdir, "sources")
|
||||
else:
|
||||
self.__data_dirs = [module_data_dir]
|
||||
self.__list, self.default = list_sources(self.__data_dirs)
|
||||
self.__override_dir = None
|
||||
self.__list, self.default = list_sources(self.__override_dir)
|
||||
if self.default is None:
|
||||
self.default = self.__list.keys()[0]
|
||||
|
|
@ -116,7 +116,7 @@ class XPCShellWorker(object):
|
|||
cmd_string = str(cmd)
|
||||
logger.debug("Sending worker message: `%s`" % cmd_string)
|
||||
try:
|
||||
self.__worker_thread.stdin.write(cmd_string + "\n")
|
||||
self.__worker_thread.stdin.write((cmd_string + "\n").encode("utf-8"))
|
||||
self.__worker_thread.stdin.flush()
|
||||
except IOError:
|
||||
logger.debug("Can't write to worker. Message `%s` wasn't heard." % cmd_string)
|
Загрузка…
Ссылка в новой задаче