sud0nick: Resolve merge conflicts for PR #50.

This commit is contained in:
Foxtrot 2019-06-19 18:09:12 +01:00
commit 06b4d28f59
23 changed files with 10355 additions and 1674 deletions

View File

@ -69,12 +69,21 @@ class Papers extends Module
case 'buildCert':
$this->buildCert($this->request->parameters);
break;
case 'encryptKey':
$this->respond($this->encryptKey($this->request->keyName, $this->request->keyType, $this->request->keyAlgo, $this->request->keyPass));
break;
case 'decryptKey':
$this->respond($this->decryptKey($this->request->keyName, $this->request->keyType, $this->request->keyPass));
break;
case 'genSSHKeys':
$this->genSSHKeys($this->request->parameters);
break;
case 'loadCertificates':
$this->loadCertificates();
break;
case 'loadCertProps':
$this->loadCertificateProperties($this->request->certName);
break;
case 'downloadKeys':
$this->downloadKeys($this->request->parameters->name, $this->request->parameters->type);
break;
@ -288,6 +297,40 @@ class Papers extends Module
$this->respond(true, "Keys created successfully!");
}
private function encryptKey($keyName, $keyType, $algo, $pass) {
$retData = array();
$argString = "encryptKeys.sh --encrypt -k " . $keyName . " -a " . $algo . " -p " . $pass;
if ($keyType == "SSH") {
$argString .= " --ssh";
}
exec(__SCRIPTS__ . $argString, $retData);
$res = implode("\n", $retData);
if ($res != "Complete") {
$this->logError("Key Encryption Error", "The following error occurred:\n\n" . $res);
return false;
}
return true;
}
private function decryptKey($keyName, $keyType, $pass) {
$retData = array();
$argString = "decryptKeys.sh -k " . $keyName . " -p " . $pass;
if ($keyType == "SSH") {
$argString .= " --ssh";
}
exec(__SCRIPTS__ . $argString, $retData);
$res = implode("\n", $retData);
if ($res != "Complete") {
$this->logError("Key Decryption Error", "The following error occurred:\n\n" . $res);
return false;
}
return true;
}
/*
Generates an OpenSSL config file based on the passed in requirements ($req)
and returns the path to the file.
@ -327,6 +370,29 @@ class Papers extends Module
$this->respond(true,null,$certs);
}
private function loadCertificateProperties($cert) {
$retData = array();
$res = [];
exec(__SCRIPTS__ . "getCertInfo.sh -k " . $cert, $retData);
if (count($retData) == 0) {
$this->respond(false);
return false;
}
// Create a mapping of the values that can be passed back to the front end
foreach ($retData as $line) {
$parts = explode("=", $line, 2);
$key = $parts[0];
$val = $parts[1];
$res[$key] = $val;
}
// Return success and the contents of the tmp file
$this->respond(true, null, $res);
return true;
}
private function getKeys($dir) {
$keyType = ($dir == __SSLSTORE__) ? "TLS/SSL" : "SSH";
$keys = scandir($dir);
@ -603,7 +669,17 @@ class Papers extends Module
fclose($fh);
}
private function retrieveLog($logname, $type) {
$dir = ($type == "error") ? __LOGS__ : ($type == "help") ? __HELPFILES__ : __CHANGELOGS__;
switch($type) {
case "error":
$dir = __LOGS__;
break;
case "help":
$dir = __HELPFILES__;
break;
default:
$dir = __CHANGELOGS__;
break;
}
$data = file_get_contents($dir . $logname);
if (!$data) {
$this->respond(false);

View File

@ -0,0 +1,5 @@
December 28, 2018<br /></br >
- Added feature to encrypt/decrypt existing private keys<br />
- Added feature to view certificate properties in the web console<br />
- Fixed a bug that prevented error logs from loading in the web console<br />
- Removed null alert when enabling SSH keys<br />

View File

@ -1,5 +1,5 @@
<strong>Encrypted</strong><br />
Displays if the private key (.pem) is encrypted. Does not include encryption on .pfx containers.
Displays if the private key is encrypted. Does not include encryption on .pfx containers.
<br /><br />
<strong>PineSSL / PineSSH</strong><br />
@ -13,6 +13,15 @@ For SSH key pairs this button has two modes (the state does not change for TLS/S
</ul>
<br />
<strong>Encrypt / Decrypt</strong><br />
If this icon is highlighted green the private key is encrypted. Click the icon and enter the key's password to decrypt it.<br />
If the icon is not highlighted it is not encrypted. Click the icon, select an algorithm, and enter a password to encrypt it.
<br /><br />
<strong>View Certificate</strong><br />
View the human readable properties of the certificate.
<br /><br />
<strong>Download</strong><br />
Packages the keys in the selected row into a zip archive and downloads it.
<br /><br />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,72 @@
#!/bin/sh
# Author: sud0nick
# Date: Dec 2018
# Location of SSL keys
ssl_store="/pineapple/modules/Papers/includes/ssl/";
ssh_store="/pineapple/modules/Papers/includes/ssh/";
help() {
echo "Decryption script for OpenSSL keys";
echo "Usage: ./decryptKeys.sh <opts>";
echo "Use './decryptKeys.sh --examples' to see example commands";
echo '';
echo 'NOTE:';
echo "Current SSL store is at $ssl_store";
echo '';
echo 'Parameters:';
echo '';
echo -e '\t-k:\tName of key to be decrypted';
echo -e '\t-p:\tPassword to use to unlock the key';
echo -e '\t--ssh:\tThe key to encrypt is in the SSH store';
echo -e '\t--help:\tDisplays this help info';
echo '';
}
examples() {
echo '';
echo 'Examples:';
echo 'Decrypt private key:';
echo './decryptKeys.sh -k keyName -p password';
echo '';
echo '';
}
if [ "$#" -lt 1 ]; then
help;
exit;
fi
while [ "$#" -gt 0 ]
do
if [[ "$1" == "--examples" ]]; then
examples;
exit;
fi
if [[ "$1" == "--ssh" ]]; then
ssl_store=$ssh_store;
fi
if [[ "$1" == "--help" ]]; then
help;
exit;
fi
if [[ "$1" == "-k" ]]; then
KEY="$2";
fi
if [[ "$1" == "-p" ]]; then
PASS="$2";
fi
shift
done;
# Generate a password on the private key
openssl rsa -in $ssl_store$KEY.key -out $ssl_store$KEY.key -passin pass:"$PASS" 2>/dev/null;
if [[ $? != 0 ]]; then
echo "Bad Password";
exit;
fi
echo "Complete"

View File

@ -5,6 +5,7 @@
# Location of SSL keys
ssl_store="/pineapple/modules/Papers/includes/ssl/";
ssh_store="/pineapple/modules/Papers/includes/ssh/";
help() {
echo "Encryption/Export script for OpenSSL certificates";
@ -21,6 +22,7 @@ help() {
echo 'Encryption Options:';
echo '';
echo -e '\t--encrypt:\tMust be supplied to encrypt keys';
echo -e '\t--ssh:\tThe key to encrypt is in the SSH store';
echo -e '\t-a:\t\tAlgorithm to use for key encryption (aes256, 3des, camellia256, etc)';
echo -e '\t-p:\t\tPassword to use for encryption';
echo '';
@ -66,8 +68,11 @@ fi
if [[ "$1" == "--encrypt" ]]; then
ENCRYPT_KEYS=true;
fi
if [[ "$1" == "--ssh" ]]; then
ssl_store=$ssh_store;
fi
if [[ "$1" == "-a" ]]; then
ALGO="$2";
ALGO="$2";
fi
if [[ "$1" == "-k" ]]; then
KEY="$2";

View File

@ -0,0 +1,51 @@
#!/bin/sh
# Author: sud0nick
# Date: Dec 2018
# Location of SSL keys
ssl_store="/pineapple/modules/Papers/includes/ssl/";
help() {
echo "Get certificate properties via OpenSSL";
echo "Usage: ./getCertInfo.sh <opts>";
echo '';
echo 'NOTE:';
echo "Current SSL store is at $ssl_store";
echo '';
echo 'Parameters:';
echo '';
echo -e '\t-k:\tKey from which to retrieve properties';
echo '';
}
if [ "$#" -lt 1 ]; then
help;
exit;
fi
while [ "$#" -gt 0 ]
do
if [[ "$1" == "-k" ]]; then
KEY="$ssl_store$2.cer";
fi
shift
done;
ISSUER=$(openssl x509 -in $KEY -noout -issuer | sed 's/^[^=]*=//g');
FINGERPRINT=$(openssl x509 -in $KEY -noout -fingerprint | sed 's/^[^=]*=//g');
SUBJECT=$(openssl x509 -in $KEY -noout -subject | sed 's/^[^=]*=//g');
START_DATE=$(openssl x509 -in $KEY -noout -startdate | sed 's/^[^=]*=//g');
END_DATE=$(openssl x509 -in $KEY -noout -enddate | sed 's/^[^=]*=//g');
SERIAL=$(openssl x509 -in $KEY -noout -serial | sed 's/^[^=]*=//g');
ALT_NAMES=$(openssl x509 -in $KEY -noout -text | grep DNS | sed 's/^[^:]*://g');
echo "issuer=$ISSUER";
echo "fingerprint=$FINGERPRINT";
echo "subject=$SUBJECT";
echo "start=$START_DATE";
echo "end=$END_DATE";
echo "serial=$SERIAL";
echo "dns=$ALT_NAMES";

View File

@ -34,6 +34,13 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
$scope.dependsProcessing = false;
$scope.selectedFiles = [];
$scope.uploading = false;
$scope.selectedKey = '';
$scope.certDecryptPassword = '';
$scope.encrypting = false;
$scope.decrypting = false;
$scope.viewCert = '';
$scope.selectedCert = '';
$scope.loadingCert = false;
$scope.checkDepends = (function(){
$api.request({
@ -159,6 +166,7 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
action: 'buildCert',
parameters: params
},function(response) {
$scope.certKeyName = '';
$scope.getLogs();
$scope.showBuildThrobber = false;
$scope.loadCertificates();
@ -166,6 +174,94 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
});
}
});
$scope.loadCertProps = (function(cert){
$scope.loadingCert = true;
$scope.viewCert = '';
$scope.selectedCert = cert;
$api.request({
module: 'Papers',
action: 'loadCertProps',
certName: cert
},function(response){
$scope.loadingCert = false;
if (response === false) {
$('#viewCert').modal('hide');
return;
}
$scope.viewCert = response.data;
});
});
$scope.selectKey = (function(key, type) {
$scope.certEncryptAlgo = "aes256";
$scope.certEncryptPassword = '';
$scope.selectedKey = key;
$scope.selectedKeyType = type;
});
$scope.encryptKey = (function(name, type, algo, pass) {
if (pass.length == 0) {
return;
}
$scope.encrypting = true;
$api.request({
module: 'Papers',
action: 'encryptKey',
keyName: name,
keyType: type,
keyAlgo: algo,
keyPass: pass
},function(response){
$scope.encrypting = false;
$scope.certEncryptPassword = '';
if (response.success === false) {
alert("Failed to encrypt key. Check the logs for more info.");
$scope.getLogs();
return;
}
$scope.loadCertificates();
$('#encryptModal').modal('hide');
});
});
$scope.decryptKey = (function(name, type, pass) {
if (pass.length == 0) {
return;
}
$scope.decrypting = true;
$api.request({
module: 'Papers',
action: 'decryptKey',
keyName: name,
keyType: type,
keyPass: pass
},function(response){
$scope.decrypting = false;
$scope.certDecryptPassword = '';
if (response.success === false) {
alert("Failed to decrypt key. Ensure you have entered the password correctly.");
$scope.getLogs();
return;
}
$scope.loadCertificates();
$('#decryptModal').modal('hide');
});
});
$scope.clearForm = (function() {
$scope.certKeyType = "tls_ssl";
@ -263,7 +359,6 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
}
} else {
// Alert error
alert(response.message);
}
$scope.loadCertificates();
});
@ -350,6 +445,7 @@ registerController('PapersController', ['$api', '$scope', '$sce', '$http', funct
parameters: log,
type: type
},function(response){
console.log(response);
if (response.success === true) {
$scope.currentLogData = $sce.trustAsHtml(response.data);
}

View File

@ -113,6 +113,14 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
<td>
<button type="button" class="btn btn-sm papers_hoverDanger" ng-show="data.Authorized==true" ng-click="revokeSSHKey(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-205-unlock.png"/></button>
<button type="button" class="btn btn-sm papers_hoverSuccess" ng-disabled="data.Encrypted == 'Yes' && data.KeyType == 'TLS/SSL'" ng-show="data.Authorized==false" ng-click="securePineapple(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-204-lock.png"/></button>
<!-- Encrypt button -->
<button type="button" class="btn btn-sm papers_hoverSuccess" data-toggle="modal" data-target="#encryptModal" ng-show="data.Encrypted == 'No'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
<!-- Decrypt button -->
<button type="button" class="btn btn-sm btn-success papers_hoverDanger" data-toggle="modal" data-target="#decryptModal" ng-show="data.Encrypted == 'Yes'" ng-click="selectKey(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-45-keys.png"/></button>
<button type="button" class="btn btn-sm papers_hoverInfo" ng-disabled="data.KeyType == 'SSH'" data-toggle="modal" data-target="#viewCert" ng-click="loadCertProps(data.Name);"><img src="/modules/Papers/includes/icons/glyphicons-28-search.png"/></button>
<button type="button" class="btn btn-sm papers_hoverInfo" ng-click="downloadKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-201-download.png"/></button>
<button type="button" class="btn btn-sm papers_hoverDanger" ng-click="deleteKeys(data.Name, data.KeyType);"><img src="/modules/Papers/includes/icons/glyphicons-17-bin.png"/></button>
</td>
@ -200,8 +208,8 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
<hr />
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
<label class="col-md-2 control-label">Signature Algorithm</label>
<div class="col-md-8">
<select name="sigalgo" ng-model="certSigAlgo" required>
<div class="col-md-6">
<select class="form-control" style="width: auto" name="sigalgo" ng-model="certSigAlgo" required>
<option value="sha1">SHA-1</option>
<option value="sha256">SHA-256</option>
<option value="sha512">SHA-512</option>
@ -269,15 +277,12 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
<div class="form-group" ng-show="certKeyType=='tls_ssl'" ng-hide="certKeyType=='ssh'">
<label class="col-md-2 control-label">Algorithm</label>
<div class="col-md-8">
<select name="algo" ng-model="certEncryptAlgo">
<select class="form-control" style="width:auto" name="algo" ng-model="certEncryptAlgo">
<option value="aes128">AES 128</option>
<option value="aes192">AES 192</option>
<option value="aes256">AES 256</option>
<option value="des">DES</option>
<option value="des3">3DES</option>
<option value="camellia128">Camellia 128</option>
<option value="camellia192">Camellia 192</option>
<option value="camellia256">Camellia 256</option>
</select>
</div>
</div>
@ -298,15 +303,12 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
<div class="form-group">
<label class="col-md-2 control-label">Container Algorithm</label>
<div class="col-md-8">
<select name="containerAlgo" ng-model="certEncryptPKCS12Algo">
<select class="form-control" style="width:auto" name="containerAlgo" ng-model="certEncryptPKCS12Algo">
<option value="aes128">AES 128</option>
<option value="aes192">AES 192</option>
<option value="aes256">AES 256</option>
<option value="des">DES</option>
<option value="des3">3DES</option>
<option value="camellia128">Camellia 128</option>
<option value="camellia192">Camellia 192</option>
<option value="camellia256">Camellia 256</option>
</select>
</div>
</div>
@ -323,6 +325,62 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
</div>
</div>
<div id="viewCert" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Certificate Properties: {{ selectedCert }}</h3>
</div>
<div class="modal-body">
<img ng-show="loadingCert" ng-hide="!loadingCert" src='/img/throbber.gif'/>
<table class="table table-striped" ng-show="!loadingCert" ng-hide="loadingCert">
<tbody>
<tr>
<td>Serial</td>
<td>{{ viewCert.serial }}</td>
</tr>
<tr>
<td>Subject</td>
<td>{{ viewCert.subject }}</td>
</tr>
<tr>
<td>Issuer</td>
<td>{{ viewCert.issuer }}</td>
</tr>
<tr>
<td>Subject Alt Names</td>
<td>{{ viewCert.dns }}</td>
</tr>
<tr>
<td>Start Date</td>
<td>{{ viewCert.start }}</td>
</tr>
<tr>
<td>End Date</td>
<td>{{ viewCert.end }}</td>
</tr>
<tr>
<td>Fingerprint</td>
<td>{{ viewCert.fingerprint }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div id="viewLogInfo" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
@ -336,7 +394,75 @@ $(document).on('mouseenter', '.papers_hoverDanger', function() {
</div>
</div>
</div>
<div id="encryptModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Key Encryption : {{ selectedKey }}</h3>
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="form-group">
<label class="col-md-3 control-label">Algorithm</label>
<div class="col-md-9">
<select class="form-control" style="width:auto" name="algo" ng-model="certEncryptAlgo">
<option value="aes128">AES 128</option>
<option value="aes192">AES 192</option>
<option value="aes256">AES 256</option>
<option value="des">DES</option>
<option value="des3">3DES</option>
</select>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label">Key Password</label>
<div class="col-md-9">
<input type="password" class="form-control" ng-model="certEncryptPassword">
</div>
</div>
<div class="form-group">
<div class="col-md-12 text-center">
<img ng-show="encrypting" ng-hide="!encrypting" src='/img/throbber.gif'/>
<button type="button" class="btn papers_hoverInfo" ng-show="!encrypting" ng-hide="encrypting" ng-disabled="certEncryptPassword == ''" ng-click="encryptKey(selectedKey, selectedKeyType, certEncryptAlgo, certEncryptPassword);" style="width: 250px">Encrypt</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="decryptModal" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h3>Key Decryption : {{ selectedKey }}</h3>
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="form-group">
<label class="col-md-3 control-label">Key Password</label>
<div class="col-md-9">
<input type="password" class="form-control" ng-model="certDecryptPassword">
</div>
</div>
<div class="form-group">
<div class="col-md-12 text-center">
<img ng-show="decrypting" ng-hide="!decrypting" src='/img/throbber.gif'/>
<button type="button" class="btn papers_hoverInfo" ng-show="!decrypting" ng-hide="decrypting" ng-disabled="certDecryptPassword == ''" ng-click="decryptKey(selectedKey, selectedKeyType, certDecryptPassword);" style="width: 250px">Decrypt</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading pointer" data-toggle="collapse" data-target="#papers_errorLogs">

View File

@ -6,5 +6,5 @@
"tetra"
],
"title": "Papers",
"version": "1.5"
"version": "1.6"
}

View File

@ -1,2 +1,2 @@
# PortalAuth
Captive portal cloner and payload distributor for the WiFi Pineapple NANO and TETRA
# PortalAuth
Captive portal cloner and payload distributor for the WiFi Pineapple NANO and TETRA

View File

@ -0,0 +1,3 @@
July 26, 2018
<br /><br />
- Fixed tinycss lib reference<br />

View File

@ -0,0 +1,5 @@
Dec 25, 2018
<br /><br />
- Moved captive portal test page to AWS S3<br />
- Added exception handling to CSS parser<br />
- Fixed typo in PortalCloner.py

View File

@ -153,23 +153,26 @@ class PortalCloner:
# Search for CSS files linked with the @import statement and remove
for style in self.soup.find_all("style"):
parser = tinycss.make_parser('page3')
stylesheet = parser.parse_stylesheet(style.string)
for rule in stylesheet.rules:
if rule.at_keyword == "@import":
# Get the name of the resource
fname = str(rule.uri).split("/")[-1]
try:
stylesheet = parser.parse_stylesheet(style.string)
for rule in stylesheet.rules:
if rule.at_keyword == "@import":
# Download the resource
resourceURLs.append([rul.uri, fname])
# Parse the CSS to get image links
_key = "resources/" + fname
self.css_urls[_key] = self.parseCSS(urlparse.urljoin(self.url, rule.uri))
# Replace the old link of the CSS with the new one
modStyle = style.string
style.string.replace_with(modStyle.replace(rule.uri, "resources/" + fname))
# Get the name of the resource
fname = str(rule.uri).split("/")[-1]
# Download the resource
resourceURLs.append([rule.uri, fname])
# Parse the CSS to get image links
_key = "resources/" + fname
self.css_urls[_key] = self.parseCSS(urlparse.urljoin(self.url, rule.uri))
# Replace the old link of the CSS with the new one
modStyle = style.string
style.string.replace_with(modStyle.replace(rule.uri, "resources/" + fname))
except:
pass
# Find and download all images and CSS files linked with <link>

File diff suppressed because it is too large Load Diff

View File

@ -1,310 +1,310 @@
# Copyright 2001-2005 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Vinay Sajip
# not be used in advertising or publicity pertaining to distribution
# of the software without specific, written prior permission.
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Configuration functions for the logging package for Python. The core package
is based on PEP 282 and comments thereto in comp.lang.python, and influenced
by Apache's log4j system.
Should work under Python versions >= 1.5.2, except that source line
information is not available unless 'sys._getframe()' is.
Copyright (C) 2001-2004 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging' and log away!
"""
import sys, logging, logging.handlers, string, socket, struct, os
try:
import thread
import threading
except ImportError:
thread = None
from SocketServer import ThreadingTCPServer, StreamRequestHandler
DEFAULT_LOGGING_CONFIG_PORT = 9030
if sys.platform == "win32":
RESET_ERROR = 10054 #WSAECONNRESET
else:
RESET_ERROR = 104 #ECONNRESET
#
# The following code implements a socket listener for on-the-fly
# reconfiguration of logging.
#
# _listener holds the server object doing the listening
_listener = None
def fileConfig(fname, defaults=None):
"""
Read the logging configuration from a ConfigParser-format file.
This can be called several times from an application, allowing an end user
the ability to select from various pre-canned configurations (if the
developer provides a mechanism to present the choices and load the chosen
configuration).
In versions of ConfigParser which have the readfp method [typically
shipped in 2.x versions of Python], you can pass in a file-like object
rather than a filename, in which case the file-like object will be read
using readfp.
"""
import ConfigParser
cp = ConfigParser.ConfigParser(defaults)
if hasattr(cp, 'readfp') and hasattr(fname, 'readline'):
cp.readfp(fname)
else:
cp.read(fname)
#first, do the formatters...
flist = cp.get("formatters", "keys")
if len(flist):
flist = string.split(flist, ",")
formatters = {}
for form in flist:
sectname = "formatter_%s" % form
opts = cp.options(sectname)
if "format" in opts:
fs = cp.get(sectname, "format", 1)
else:
fs = None
if "datefmt" in opts:
dfs = cp.get(sectname, "datefmt", 1)
else:
dfs = None
f = logging.Formatter(fs, dfs)
formatters[form] = f
#next, do the handlers...
#critical section...
logging._acquireLock()
try:
try:
#first, lose the existing handlers...
logging._handlers.clear()
#now set up the new ones...
hlist = cp.get("handlers", "keys")
if len(hlist):
hlist = string.split(hlist, ",")
handlers = {}
fixups = [] #for inter-handler references
for hand in hlist:
try:
sectname = "handler_%s" % hand
klass = cp.get(sectname, "class")
opts = cp.options(sectname)
if "formatter" in opts:
fmt = cp.get(sectname, "formatter")
else:
fmt = ""
klass = eval(klass, vars(logging))
args = cp.get(sectname, "args")
args = eval(args, vars(logging))
h = apply(klass, args)
if "level" in opts:
level = cp.get(sectname, "level")
h.setLevel(logging._levelNames[level])
if len(fmt):
h.setFormatter(formatters[fmt])
#temporary hack for FileHandler and MemoryHandler.
if klass == logging.handlers.MemoryHandler:
if "target" in opts:
target = cp.get(sectname,"target")
else:
target = ""
if len(target): #the target handler may not be loaded yet, so keep for later...
fixups.append((h, target))
handlers[hand] = h
except: #if an error occurs when instantiating a handler, too bad
pass #this could happen e.g. because of lack of privileges
#now all handlers are loaded, fixup inter-handler references...
for fixup in fixups:
h = fixup[0]
t = fixup[1]
h.setTarget(handlers[t])
#at last, the loggers...first the root...
llist = cp.get("loggers", "keys")
llist = string.split(llist, ",")
llist.remove("root")
sectname = "logger_root"
root = logging.root
log = root
opts = cp.options(sectname)
if "level" in opts:
level = cp.get(sectname, "level")
log.setLevel(logging._levelNames[level])
for h in root.handlers[:]:
root.removeHandler(h)
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
log.addHandler(handlers[hand])
#and now the others...
#we don't want to lose the existing loggers,
#since other threads may have pointers to them.
#existing is set to contain all existing loggers,
#and as we go through the new configuration we
#remove any which are configured. At the end,
#what's left in existing is the set of loggers
#which were in the previous configuration but
#which are not in the new configuration.
existing = root.manager.loggerDict.keys()
#now set up the new ones...
for log in llist:
sectname = "logger_%s" % log
qn = cp.get(sectname, "qualname")
opts = cp.options(sectname)
if "propagate" in opts:
propagate = cp.getint(sectname, "propagate")
else:
propagate = 1
logger = logging.getLogger(qn)
if qn in existing:
existing.remove(qn)
if "level" in opts:
level = cp.get(sectname, "level")
logger.setLevel(logging._levelNames[level])
for h in logger.handlers[:]:
logger.removeHandler(h)
logger.propagate = propagate
logger.disabled = 0
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
logger.addHandler(handlers[hand])
#Disable any old loggers. There's no point deleting
#them as other threads may continue to hold references
#and by disabling them, you stop them doing any logging.
for log in existing:
root.manager.loggerDict[log].disabled = 1
except:
import traceback
ei = sys.exc_info()
traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
del ei
finally:
logging._releaseLock()
def listen(port=DEFAULT_LOGGING_CONFIG_PORT):
"""
Start up a socket server on the specified port, and listen for new
configurations.
These will be sent as a file suitable for processing by fileConfig().
Returns a Thread object on which you can call start() to start the server,
and which you can join() when appropriate. To stop the server, call
stopListening().
"""
if not thread:
raise NotImplementedError, "listen() needs threading to work"
class ConfigStreamHandler(StreamRequestHandler):
"""
Handler for a logging configuration request.
It expects a completely new logging configuration and uses fileConfig
to install it.
"""
def handle(self):
"""
Handle a request.
Each request is expected to be a 4-byte length,
followed by the config file. Uses fileConfig() to do the
grunt work.
"""
import tempfile
try:
conn = self.connection
chunk = conn.recv(4)
if len(chunk) == 4:
slen = struct.unpack(">L", chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + conn.recv(slen - len(chunk))
#Apply new configuration. We'd like to be able to
#create a StringIO and pass that in, but unfortunately
#1.5.2 ConfigParser does not support reading file
#objects, only actual files. So we create a temporary
#file and remove it later.
file = tempfile.mktemp(".ini")
f = open(file, "w")
f.write(chunk)
f.close()
fileConfig(file)
os.remove(file)
except socket.error, e:
if type(e.args) != types.TupleType:
raise
else:
errcode = e.args[0]
if errcode != RESET_ERROR:
raise
class ConfigSocketReceiver(ThreadingTCPServer):
"""
A simple TCP socket-based logging config receiver.
"""
allow_reuse_address = 1
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
handler=None):
ThreadingTCPServer.__init__(self, (host, port), handler)
logging._acquireLock()
self.abort = 0
logging._releaseLock()
self.timeout = 1
def serve_until_stopped(self):
import select
abort = 0
while not abort:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
self.timeout)
if rd:
self.handle_request()
logging._acquireLock()
abort = self.abort
logging._releaseLock()
def serve(rcvr, hdlr, port):
server = rcvr(port=port, handler=hdlr)
global _listener
logging._acquireLock()
_listener = server
logging._releaseLock()
server.serve_until_stopped()
return threading.Thread(target=serve,
args=(ConfigSocketReceiver,
ConfigStreamHandler, port))
def stopListening():
"""
Stop the listening server which was created with a call to listen().
"""
global _listener
if _listener:
logging._acquireLock()
_listener.abort = 1
_listener = None
logging._releaseLock()
# Copyright 2001-2005 by Vinay Sajip. All Rights Reserved.
#
# Permission to use, copy, modify, and distribute this software and its
# documentation for any purpose and without fee is hereby granted,
# provided that the above copyright notice appear in all copies and that
# both that copyright notice and this permission notice appear in
# supporting documentation, and that the name of Vinay Sajip
# not be used in advertising or publicity pertaining to distribution
# of the software without specific, written prior permission.
# VINAY SAJIP DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
# ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# VINAY SAJIP BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
# ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
# IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
Configuration functions for the logging package for Python. The core package
is based on PEP 282 and comments thereto in comp.lang.python, and influenced
by Apache's log4j system.
Should work under Python versions >= 1.5.2, except that source line
information is not available unless 'sys._getframe()' is.
Copyright (C) 2001-2004 Vinay Sajip. All Rights Reserved.
To use, simply 'import logging' and log away!
"""
import sys, logging, logging.handlers, string, socket, struct, os
try:
import thread
import threading
except ImportError:
thread = None
from SocketServer import ThreadingTCPServer, StreamRequestHandler
DEFAULT_LOGGING_CONFIG_PORT = 9030
if sys.platform == "win32":
RESET_ERROR = 10054 #WSAECONNRESET
else:
RESET_ERROR = 104 #ECONNRESET
#
# The following code implements a socket listener for on-the-fly
# reconfiguration of logging.
#
# _listener holds the server object doing the listening
_listener = None
def fileConfig(fname, defaults=None):
"""
Read the logging configuration from a ConfigParser-format file.
This can be called several times from an application, allowing an end user
the ability to select from various pre-canned configurations (if the
developer provides a mechanism to present the choices and load the chosen
configuration).
In versions of ConfigParser which have the readfp method [typically
shipped in 2.x versions of Python], you can pass in a file-like object
rather than a filename, in which case the file-like object will be read
using readfp.
"""
import ConfigParser
cp = ConfigParser.ConfigParser(defaults)
if hasattr(cp, 'readfp') and hasattr(fname, 'readline'):
cp.readfp(fname)
else:
cp.read(fname)
#first, do the formatters...
flist = cp.get("formatters", "keys")
if len(flist):
flist = string.split(flist, ",")
formatters = {}
for form in flist:
sectname = "formatter_%s" % form
opts = cp.options(sectname)
if "format" in opts:
fs = cp.get(sectname, "format", 1)
else:
fs = None
if "datefmt" in opts:
dfs = cp.get(sectname, "datefmt", 1)
else:
dfs = None
f = logging.Formatter(fs, dfs)
formatters[form] = f
#next, do the handlers...
#critical section...
logging._acquireLock()
try:
try:
#first, lose the existing handlers...
logging._handlers.clear()
#now set up the new ones...
hlist = cp.get("handlers", "keys")
if len(hlist):
hlist = string.split(hlist, ",")
handlers = {}
fixups = [] #for inter-handler references
for hand in hlist:
try:
sectname = "handler_%s" % hand
klass = cp.get(sectname, "class")
opts = cp.options(sectname)
if "formatter" in opts:
fmt = cp.get(sectname, "formatter")
else:
fmt = ""
klass = eval(klass, vars(logging))
args = cp.get(sectname, "args")
args = eval(args, vars(logging))
h = apply(klass, args)
if "level" in opts:
level = cp.get(sectname, "level")
h.setLevel(logging._levelNames[level])
if len(fmt):
h.setFormatter(formatters[fmt])
#temporary hack for FileHandler and MemoryHandler.
if klass == logging.handlers.MemoryHandler:
if "target" in opts:
target = cp.get(sectname,"target")
else:
target = ""
if len(target): #the target handler may not be loaded yet, so keep for later...
fixups.append((h, target))
handlers[hand] = h
except: #if an error occurs when instantiating a handler, too bad
pass #this could happen e.g. because of lack of privileges
#now all handlers are loaded, fixup inter-handler references...
for fixup in fixups:
h = fixup[0]
t = fixup[1]
h.setTarget(handlers[t])
#at last, the loggers...first the root...
llist = cp.get("loggers", "keys")
llist = string.split(llist, ",")
llist.remove("root")
sectname = "logger_root"
root = logging.root
log = root
opts = cp.options(sectname)
if "level" in opts:
level = cp.get(sectname, "level")
log.setLevel(logging._levelNames[level])
for h in root.handlers[:]:
root.removeHandler(h)
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
log.addHandler(handlers[hand])
#and now the others...
#we don't want to lose the existing loggers,
#since other threads may have pointers to them.
#existing is set to contain all existing loggers,
#and as we go through the new configuration we
#remove any which are configured. At the end,
#what's left in existing is the set of loggers
#which were in the previous configuration but
#which are not in the new configuration.
existing = root.manager.loggerDict.keys()
#now set up the new ones...
for log in llist:
sectname = "logger_%s" % log
qn = cp.get(sectname, "qualname")
opts = cp.options(sectname)
if "propagate" in opts:
propagate = cp.getint(sectname, "propagate")
else:
propagate = 1
logger = logging.getLogger(qn)
if qn in existing:
existing.remove(qn)
if "level" in opts:
level = cp.get(sectname, "level")
logger.setLevel(logging._levelNames[level])
for h in logger.handlers[:]:
logger.removeHandler(h)
logger.propagate = propagate
logger.disabled = 0
hlist = cp.get(sectname, "handlers")
if len(hlist):
hlist = string.split(hlist, ",")
for hand in hlist:
logger.addHandler(handlers[hand])
#Disable any old loggers. There's no point deleting
#them as other threads may continue to hold references
#and by disabling them, you stop them doing any logging.
for log in existing:
root.manager.loggerDict[log].disabled = 1
except:
import traceback
ei = sys.exc_info()
traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
del ei
finally:
logging._releaseLock()
def listen(port=DEFAULT_LOGGING_CONFIG_PORT):
"""
Start up a socket server on the specified port, and listen for new
configurations.
These will be sent as a file suitable for processing by fileConfig().
Returns a Thread object on which you can call start() to start the server,
and which you can join() when appropriate. To stop the server, call
stopListening().
"""
if not thread:
raise NotImplementedError, "listen() needs threading to work"
class ConfigStreamHandler(StreamRequestHandler):
"""
Handler for a logging configuration request.
It expects a completely new logging configuration and uses fileConfig
to install it.
"""
def handle(self):
"""
Handle a request.
Each request is expected to be a 4-byte length,
followed by the config file. Uses fileConfig() to do the
grunt work.
"""
import tempfile
try:
conn = self.connection
chunk = conn.recv(4)
if len(chunk) == 4:
slen = struct.unpack(">L", chunk)[0]
chunk = self.connection.recv(slen)
while len(chunk) < slen:
chunk = chunk + conn.recv(slen - len(chunk))
#Apply new configuration. We'd like to be able to
#create a StringIO and pass that in, but unfortunately
#1.5.2 ConfigParser does not support reading file
#objects, only actual files. So we create a temporary
#file and remove it later.
file = tempfile.mktemp(".ini")
f = open(file, "w")
f.write(chunk)
f.close()
fileConfig(file)
os.remove(file)
except socket.error, e:
if type(e.args) != types.TupleType:
raise
else:
errcode = e.args[0]
if errcode != RESET_ERROR:
raise
class ConfigSocketReceiver(ThreadingTCPServer):
"""
A simple TCP socket-based logging config receiver.
"""
allow_reuse_address = 1
def __init__(self, host='localhost', port=DEFAULT_LOGGING_CONFIG_PORT,
handler=None):
ThreadingTCPServer.__init__(self, (host, port), handler)
logging._acquireLock()
self.abort = 0
logging._releaseLock()
self.timeout = 1
def serve_until_stopped(self):
import select
abort = 0
while not abort:
rd, wr, ex = select.select([self.socket.fileno()],
[], [],
self.timeout)
if rd:
self.handle_request()
logging._acquireLock()
abort = self.abort
logging._releaseLock()
def serve(rcvr, hdlr, port):
server = rcvr(port=port, handler=hdlr)
global _listener
logging._acquireLock()
_listener = server
logging._releaseLock()
server.serve_until_stopped()
return threading.Thread(target=serve,
args=(ConfigSocketReceiver,
ConfigStreamHandler, port))
def stopListening():
"""
Stop the listening server which was created with a call to listen().
"""
global _listener
if _listener:
logging._acquireLock()
_listener.abort = 1
_listener = None
logging._releaseLock()

View File

@ -0,0 +1,200 @@
# coding: utf-8
"""
tinycss.colors3
---------------
Parser for CSS 3 Fonts syntax:
https://www.w3.org/TR/css-fonts-3/
Adds support for font-face and font-feature-values rules.
:copyright: (c) 2016 by Kozea.
:license: BSD, see LICENSE for more details.
"""
from __future__ import division, unicode_literals
from .css21 import CSS21Parser, ParseError
class FontFaceRule(object):
"""A parsed at-rule for font faces.
.. attribute:: at_keyword
Always ``'@font-face'``.
.. attribute:: declarations
A list of :class:`~.css21.Declaration` objects.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
"""
def __init__(self, at_keyword, declarations, line, column):
assert at_keyword == '@font-face'
self.at_keyword = at_keyword
self.declarations = declarations
self.line = line
self.column = column
class FontFeatureValuesRule(object):
"""A parsed at-rule for font feature values.
.. attribute:: at_keyword
Always ``'@font-feature-values'``.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
.. attribute:: at_rules
The list of parsed at-rules inside the @font-feature-values block, in
source order.
.. attribute:: family_names
A list of strings representing font families.
"""
def __init__(self, at_keyword, at_rules, family_names, line, column):
assert at_keyword == '@font-feature-values'
self.at_keyword = at_keyword
self.family_names = family_names
self.at_rules = at_rules
self.line = line
self.column = column
class FontFeatureRule(object):
"""A parsed at-rule for font features.
.. attribute:: at_keyword
One of the 16 following strings:
* ``@stylistic``
* ``@styleset``
* ``@character-variant``
* ``@swash``
* ``@ornaments``
* ``@annotation``
.. attribute:: declarations
A list of :class:`~.css21.Declaration` objects.
.. attribute:: line
Source line where this was read.
.. attribute:: column
Source column where this was read.
"""
def __init__(self, at_keyword, declarations, line, column):
self.at_keyword = at_keyword
self.declarations = declarations
self.line = line
self.column = column
class CSSFonts3Parser(CSS21Parser):
"""Extend :class:`~.css21.CSS21Parser` for `CSS 3 Fonts`_ syntax.
.. _CSS 3 Fonts: https://www.w3.org/TR/css-fonts-3/
"""
FONT_FEATURE_VALUES_AT_KEYWORDS = [
'@stylistic',
'@styleset',
'@character-variant',
'@swash',
'@ornaments',
'@annotation',
]
def parse_at_rule(self, rule, previous_rules, errors, context):
if rule.at_keyword == '@font-face':
if rule.head:
raise ParseError(
rule.head[0],
'unexpected {0} token in {1} rule header'.format(
rule.head[0].type, rule.at_keyword))
declarations, body_errors = self.parse_declaration_list(rule.body)
errors.extend(body_errors)
return FontFaceRule(
rule.at_keyword, declarations, rule.line, rule.column)
elif rule.at_keyword == '@font-feature-values':
family_names = tuple(
self.parse_font_feature_values_family_names(rule.head))
at_rules, body_errors = (
self.parse_rules(rule.body or [], '@font-feature-values'))
errors.extend(body_errors)
return FontFeatureValuesRule(
rule.at_keyword, at_rules, family_names,
rule.line, rule.column)
elif rule.at_keyword in self.FONT_FEATURE_VALUES_AT_KEYWORDS:
if context != '@font-feature-values':
raise ParseError(
rule, '{0} rule not allowed in {1}'.format(
rule.at_keyword, context))
declarations, body_errors = self.parse_declaration_list(rule.body)
errors.extend(body_errors)
return FontFeatureRule(
rule.at_keyword, declarations, rule.line, rule.column)
return super(CSSFonts3Parser, self).parse_at_rule(
rule, previous_rules, errors, context)
def parse_font_feature_values_family_names(self, tokens):
"""Parse an @font-feature-values selector.
:param tokens:
An iterable of token, typically from the ``head`` attribute of
an unparsed :class:`AtRule`.
:returns:
A generator of strings representing font families.
:raises:
:class:`~.parsing.ParseError` on invalid selectors
"""
family = ''
current_string = False
for token in tokens:
if token.type == 'DELIM' and token.value == ',' and family:
yield family
family = ''
current_string = False
elif token.type == 'STRING' and not family and (
current_string is False):
family = token.value
current_string = True
elif token.type == 'IDENT' and not current_string:
if family:
family += ' '
family += token.value
elif token.type != 'S':
family = ''
break
if family:
yield family
else:
raise ParseError(token, 'invalid @font-feature-values selector')

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,144 @@
# coding: utf-8
"""
Tests for the Fonts 3 parser
----------------------------
:copyright: (c) 2016 by Kozea.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import pytest
from tinycss.fonts3 import CSSFonts3Parser
from . import assert_errors
from .test_tokenizer import jsonify
@pytest.mark.parametrize(('css', 'expected_family_names', 'expected_errors'), [
('@font-feature-values foo {}', ('foo',), []),
('@font-feature-values Foo Test {}', ('Foo Test',), []),
('@font-feature-values \'Foo Test\' {}', ('Foo Test',), []),
('@font-feature-values Foo Test, Foo Lol, "Foo tooo"', (
'Foo Test', 'Foo Lol', 'Foo tooo'), []),
('@font-feature-values Foo , Foo lol {}', ('Foo', 'Foo lol'), []),
('@font-feature-values Foo , "Foobar" , Lol {}', (
'Foo', 'Foobar', 'Lol'), []),
('@font-feature-values Foo, {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values ,Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test,"Foo", {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test "Foo" {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test Foo, Test "bar", "foo" {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values Test/Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values /Foo {}', None, [
'invalid @font-feature-values selector']),
('@font-feature-values #Foo {}', None, [
'invalid @font-feature-values selector']),
# TODO: this currently works but should not work
# ('@font-feature-values test@foo {}', None, [
# 'invalid @font-feature-values selector']),
('@font-feature-values Hawaii 5-0 {}', None, [
'invalid @font-feature-values selector']),
])
def test_font_feature_values_selectors(css, expected_family_names,
expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
if stylesheet.rules:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-feature-values'
assert rule.family_names == expected_family_names
@pytest.mark.parametrize(('css', 'expected_declarations', 'expected_errors'), [
('@font-face {}', [], []),
('@font-face test { src: "lol"; font-family: "bar" }', None, [
'unexpected IDENT token in @font-face rule header']),
('@font-face { src: "lol"; font-family: "bar" }', [
('src', [('STRING', 'lol')]),
('font-family', [('STRING', 'bar')])], []),
('@font-face { src: "lol"; font-family: "bar"; src: "baz" }', [
('src', [('STRING', 'lol')]),
('font-family', [('STRING', 'bar')]),
('src', [('STRING', 'baz')])], []),
])
def test_font_face_content(css, expected_declarations, expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
def declarations(rule):
return [(decl.name, list(jsonify(decl.value)))
for decl in rule.declarations]
if expected_declarations is None:
assert stylesheet.rules == []
assert expected_errors
else:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-face'
assert declarations(rule) == expected_declarations
@pytest.mark.parametrize(
('css', 'expected_rules', 'expected_errors'), [
('''@annotation{}''', None, [
'@annotation rule not allowed in stylesheet']),
('''@font-feature-values foo {}''', None, []),
('''@font-feature-values foo {
@swash { ornate: 1; }
@styleset { double-W: 14; sharp-terminals: 16 1; }
}''', [
('@swash', [('ornate', [('INTEGER', 1)])]),
('@styleset', [
('double-w', [('INTEGER', 14)]),
('sharp-terminals', [
('INTEGER', 16), ('S', ' '), ('INTEGER', 1)])])], []),
('''@font-feature-values foo {
@swash { ornate: 14; }
@unknown { test: 1; }
}''', [('@swash', [('ornate', [('INTEGER', 14)])])], [
'unknown at-rule in @font-feature-values context: @unknown']),
('''@font-feature-values foo {
@annotation{boxed:1}
bad: 2;
@brokenstylesetbecauseofbadabove { sharp: 1}
@styleset { sharp-terminals: 16 1; @bad {}}
@styleset { @bad {} top-ignored: 3; top: 9000}
really-bad
}''', [
('@annotation', [('boxed', [('INTEGER', 1)])]),
('@styleset', [
('sharp-terminals', [
('INTEGER', 16), ('S', ' '), ('INTEGER', 1)])]),
('@styleset', [('top', [('INTEGER', 9000)])])], [
'unexpected ; token in selector',
'expected a property name, got ATKEYWORD',
'expected a property name, got ATKEYWORD',
'no declaration block found for ruleset']),
])
def test_font_feature_values_content(css, expected_rules, expected_errors):
stylesheet = CSSFonts3Parser().parse_stylesheet(css)
assert_errors(stylesheet.errors, expected_errors)
if expected_rules is not None:
assert len(stylesheet.rules) == 1
rule = stylesheet.rules[0]
assert rule.at_keyword == '@font-feature-values'
rules = [
(at_rule.at_keyword, [
(decl.name, list(jsonify(decl.value)))
for decl in at_rule.declarations])
for at_rule in rule.at_rules] if rule.at_rules else None
assert rules == expected_rules

View File

@ -0,0 +1,302 @@
# coding: utf-8
"""
Tests for the tokenizer
-----------------------
:copyright: (c) 2012 by Simon Sapin.
:license: BSD, see LICENSE for more details.
"""
from __future__ import unicode_literals
import os
import sys
import pytest
from tinycss.tokenizer import (
cython_tokenize_flat, python_tokenize_flat, regroup)
def test_speedups():
is_pypy = hasattr(sys, 'pypy_translation_info')
env_skip_tests = os.environ.get('TINYCSS_SKIP_SPEEDUPS_TESTS')
# pragma: no cover
if is_pypy or env_skip_tests:
return
assert cython_tokenize_flat is not None, (
'Cython speedups are not installed, related tests will '
'be skipped. Set the TINYCSS_SKIP_SPEEDUPS_TESTS environment '
'variable if this is expected.')
@pytest.mark.parametrize(('tokenize', 'css_source', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
('', []),
('red -->', [('IDENT', 'red'), ('S', ' '), ('CDC', '-->')]),
# Longest match rule: no CDC
('red-->', [('IDENT', 'red--'), ('DELIM', '>')]),
(r'p[example="foo(int x) { this.x = x;}"]', [
('IDENT', 'p'),
('[', '['),
('IDENT', 'example'),
('DELIM', '='),
('STRING', 'foo(int x) { this.x = x;}'),
(']', ']')]),
# Numbers are parsed
('42 .5 -4pX 1.25em 30%', [
('INTEGER', 42), ('S', ' '),
('NUMBER', .5), ('S', ' '),
# units are normalized to lower-case:
('DIMENSION', -4, 'px'), ('S', ' '),
('DIMENSION', 1.25, 'em'), ('S', ' '),
('PERCENTAGE', 30, '%')]),
# URLs are extracted
('url(foo.png)', [('URI', 'foo.png')]),
('url("foo.png")', [('URI', 'foo.png')]),
# Escaping
(r'/* Comment with a \ backslash */', [
('COMMENT', '/* Comment with a \ backslash */')]), # Unchanged
# backslash followed by a newline in a string: ignored
('"Lorem\\\nIpsum"', [('STRING', 'LoremIpsum')]),
# backslash followed by a newline outside a string: stands for itself
('Lorem\\\nIpsum', [
('IDENT', 'Lorem'), ('DELIM', '\\'),
('S', '\n'), ('IDENT', 'Ipsum')]),
# Cancel the meaning of special characters
(r'"Lore\m Ipsum"', [('STRING', 'Lorem Ipsum')]), # or not specal
(r'"Lorem \49psum"', [('STRING', 'Lorem Ipsum')]),
(r'"Lorem \49 psum"', [('STRING', 'Lorem Ipsum')]),
(r'"Lorem\"Ipsum"', [('STRING', 'Lorem"Ipsum')]),
(r'"Lorem\\Ipsum"', [('STRING', r'Lorem\Ipsum')]),
(r'"Lorem\5c Ipsum"', [('STRING', r'Lorem\Ipsum')]),
(r'Lorem\+Ipsum', [('IDENT', 'Lorem+Ipsum')]),
(r'Lorem+Ipsum', [
('IDENT', 'Lorem'), ('DELIM', '+'), ('IDENT', 'Ipsum')]),
(r'url(foo\).png)', [('URI', 'foo).png')]),
# Unicode and backslash escaping
('\\26 B', [('IDENT', '&B')]),
('\\&B', [('IDENT', '&B')]),
('@\\26\tB', [('ATKEYWORD', '@&B')]),
('@\\&B', [('ATKEYWORD', '@&B')]),
('#\\26\nB', [('HASH', '#&B')]),
('#\\&B', [('HASH', '#&B')]),
('\\26\r\nB(', [('FUNCTION', '&B(')]),
('\\&B(', [('FUNCTION', '&B(')]),
(r'12.5\000026B', [('DIMENSION', 12.5, '&b')]),
(r'12.5\0000263B', [('DIMENSION', 12.5, '&3b')]), # max 6 digits
(r'12.5\&B', [('DIMENSION', 12.5, '&b')]),
(r'"\26 B"', [('STRING', '&B')]),
(r"'\000026B'", [('STRING', '&B')]),
(r'"\&B"', [('STRING', '&B')]),
(r'url("\26 B")', [('URI', '&B')]),
(r'url(\26 B)', [('URI', '&B')]),
(r'url("\&B")', [('URI', '&B')]),
(r'url(\&B)', [('URI', '&B')]),
(r'Lorem\110000Ipsum', [('IDENT', 'Lorem\uFFFDIpsum')]),
# Bad strings
# String ends at EOF without closing: no error, parsed
('"Lorem\\26Ipsum', [('STRING', 'Lorem&Ipsum')]),
# Unescaped newline: ends the string, error, unparsed
('"Lorem\\26Ipsum\n', [
('BAD_STRING', r'"Lorem\26Ipsum'), ('S', '\n')]),
# Tokenization restarts after the newline, so the second " starts
# a new string (which ends at EOF without errors, as above.)
('"Lorem\\26Ipsum\ndolor" sit', [
('BAD_STRING', r'"Lorem\26Ipsum'), ('S', '\n'),
('IDENT', 'dolor'), ('STRING', ' sit')]),
]])
def test_tokens(tokenize, css_source, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
sources = [css_source]
if sys.version_info[0] < 3:
# On Python 2.x, ASCII-only bytestrings can be used
# where Unicode is expected.
sources.append(css_source.encode('ascii'))
for css_source in sources:
tokens = tokenize(css_source, ignore_comments=False)
result = [
(token.type, token.value) + (
() if token.unit is None else (token.unit,))
for token in tokens
]
assert result == expected_tokens
@pytest.mark.parametrize('tokenize', [
python_tokenize_flat, cython_tokenize_flat])
def test_positions(tokenize):
"""Test the reported line/column position of each token."""
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
css = '/* Lorem\nipsum */\fa {\n color: red;\tcontent: "dolor\\\fsit" }'
tokens = tokenize(css, ignore_comments=False)
result = [(token.type, token.line, token.column) for token in tokens]
assert result == [
('COMMENT', 1, 1), ('S', 2, 9),
('IDENT', 3, 1), ('S', 3, 2), ('{', 3, 3),
('S', 3, 4), ('IDENT', 4, 5), (':', 4, 10),
('S', 4, 11), ('IDENT', 4, 12), (';', 4, 15), ('S', 4, 16),
('IDENT', 4, 17), (':', 4, 24), ('S', 4, 25), ('STRING', 4, 26),
('S', 5, 5), ('}', 5, 6)]
@pytest.mark.parametrize(('tokenize', 'css_source', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
('', []),
(r'Lorem\26 "i\psum"4px', [
('IDENT', 'Lorem&'), ('STRING', 'ipsum'), ('DIMENSION', 4)]),
('not([[lorem]]{ipsum (42)})', [
('FUNCTION', 'not', [
('[', [
('[', [
('IDENT', 'lorem'),
]),
]),
('{', [
('IDENT', 'ipsum'),
('S', ' '),
('(', [
('INTEGER', 42),
])
])
])]),
# Close everything at EOF, no error
('a[b{"d', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('STRING', 'd'),
]),
]),
]),
# Any remaining ), ] or } token is a nesting error
('a[b{d]e}', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('IDENT', 'd'),
(']', ']'), # The error is visible here
('IDENT', 'e'),
]),
]),
]),
# ref:
('a[b{d}e]', [
('IDENT', 'a'),
('[', [
('IDENT', 'b'),
('{', [
('IDENT', 'd'),
]),
('IDENT', 'e'),
]),
]),
]])
def test_token_grouping(tokenize, css_source, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
tokens = regroup(tokenize(css_source, ignore_comments=False))
result = list(jsonify(tokens))
assert result == expected_tokens
def jsonify(tokens):
"""Turn tokens into "JSON-compatible" data structures."""
for token in tokens:
if token.type == 'FUNCTION':
yield (token.type, token.function_name,
list(jsonify(token.content)))
elif token.is_container:
yield token.type, list(jsonify(token.content))
else:
yield token.type, token.value
@pytest.mark.parametrize(('tokenize', 'ignore_comments', 'expected_tokens'), [
(tokenize,) + test_data
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
(False, [
('COMMENT', '/* lorem */'),
('S', ' '),
('IDENT', 'ipsum'),
('[', [
('IDENT', 'dolor'),
('COMMENT', '/* sit */'),
]),
('BAD_COMMENT', '/* amet')
]),
(True, [
('S', ' '),
('IDENT', 'ipsum'),
('[', [
('IDENT', 'dolor'),
]),
]),
]])
def test_comments(tokenize, ignore_comments, expected_tokens):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
css_source = '/* lorem */ ipsum[dolor/* sit */]/* amet'
tokens = regroup(tokenize(css_source, ignore_comments))
result = list(jsonify(tokens))
assert result == expected_tokens
@pytest.mark.parametrize(('tokenize', 'css_source'), [
(tokenize, test_data)
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
r'p[example="foo(int x) { this.x = x;}"]',
'"Lorem\\26Ipsum\ndolor" sit',
'/* Lorem\nipsum */\fa {\n color: red;\tcontent: "dolor\\\fsit" }',
'not([[lorem]]{ipsum (42)})',
'a[b{d]e}',
'a[b{"d',
]])
def test_token_serialize_css(tokenize, css_source):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
for _regroup in [regroup, lambda x: x]:
tokens = _regroup(tokenize(css_source, ignore_comments=False))
result = ''.join(token.as_css() for token in tokens)
assert result == css_source
@pytest.mark.parametrize(('tokenize', 'css_source'), [
(tokenize, test_data)
for tokenize in (python_tokenize_flat, cython_tokenize_flat)
for test_data in [
'(8, foo, [z])', '[8, foo, (z)]', '{8, foo, [z]}', 'func(8, foo, [z])'
]
])
def test_token_api(tokenize, css_source):
if tokenize is None: # pragma: no cover
pytest.skip('Speedups not available')
tokens = list(regroup(tokenize(css_source)))
assert len(tokens) == 1
token = tokens[0]
expected_len = 7 # 2 spaces, 2 commas, 3 others.
assert len(token.content) == expected_len

View File

@ -0,0 +1 @@
VERSION = '0.4'

View File

@ -113,7 +113,7 @@ registerController('PortalAuthController', ['$api', '$scope', '$sce', '$interval
configs['dataExpected'] = $scope.dataExpected;
configs['p_archive'] = $scope.portalArchive;
} else {
configs['testSite'] = "https://www.puffycode.com/cptest.html";
configs['testSite'] = "https://portalauth.s3.us-east-2.amazonaws.com/cptest.html";
configs['dataExpected'] = "No Captive Portal";
configs['p_archive'] = "/root/portals/";
}

View File

@ -6,5 +6,5 @@
"tetra"
],
"title": "Portal Auth",
"version": "1.7"
}
"version": "1.9"
}