Skip to content

Commit 62449cb

Browse files
committed
new recipe isotoma.recipe.apache
1 parent 7b59cc6 commit 62449cb

File tree

9 files changed

+401
-0
lines changed

9 files changed

+401
-0
lines changed

isotoma.recipe.apache/CHANGES.txt

Whitespace-only changes.

isotoma.recipe.apache/README.rst

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
Apache buildout recipe
2+
======================
3+
4+
This package provides buildout_ recipes for the configuration of apache. This
5+
has a number of features that we have found useful in production, such as
6+
support for long CA chains, htpasswd authentication protection and the support
7+
for optional templates provided with the buildout.
8+
9+
We use the system apache, so this recipe will not install apache for you. If
10+
you wish to install apache, use `zc.recipe.cmmi`_ perhaps.
11+
12+
.. _buildout: http://pypi.python.org/pypi/zc.buildout
13+
.. _`zc.recipe.cmmi`: http://pypi.python.org/pypi/zc.recipe.cmmi
14+
15+
16+
Mandatory Parameters
17+
--------------------
18+
19+
interface
20+
The IP address of the interface on which to listen
21+
sitename
22+
The name of the site, used to identify the correct virtual host
23+
serveradmin
24+
The email address of the administrator of the server
25+
proxyport
26+
The port to which requests are forwarded
27+
28+
Optional Parameters
29+
-------------------
30+
31+
realm
32+
The name of the HTTP Authentication realm, if you wish to password protect this site
33+
passwdfile
34+
The filename of the htpasswd file to secure the realm, defaults to "passwd" in the part directory
35+
username
36+
The username used in the htpasswd file
37+
allowpurge
38+
The IP address of a server that is permitted to send PURGE requests to this server
39+
portal
40+
The name of the portal object in the zope server, defaults to "portal"
41+
template
42+
The filename of the template file to use, if you do not wish to use the default
43+
configfile
44+
The name of the config file written by the recipe, defaults to "apache.cfg" in the part directory
45+
sslca
46+
A list of full pathnames to certificate authority certificate files
47+
sslcert
48+
The full pathname of the ssl certificate, if required
49+
sslkey
50+
The full pathname of the key for the ssl certificate
51+
52+
License
53+
-------
54+
55+
Copyright 2010 Isotoma Limited
56+
57+
Licensed under the Apache License, Version 2.0 (the "License");
58+
you may not use this file except in compliance with the License.
59+
You may obtain a copy of the License at
60+
61+
http://www.apache.org/licenses/LICENSE-2.0
62+
63+
Unless required by applicable law or agreed to in writing, software
64+
distributed under the License is distributed on an "AS IS" BASIS,
65+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
66+
See the License for the specific language governing permissions and
67+
limitations under the License.
68+
69+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__import__('pkg_resources').declare_namespace(__name__)
2+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__import__('pkg_resources').declare_namespace(__name__)
2+
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2010 Isotoma Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import os
17+
import zc.buildout
18+
from Cheetah.Template import Template
19+
20+
import htpasswd
21+
22+
def sibpath(filename):
23+
return os.path.join(os.path.dirname(__file__), filename)
24+
25+
class Apache(object):
26+
27+
template = os.path.join(os.path.dirname(__file__), "apache.cfg")
28+
ssltemplate = os.path.join(os.path.dirname(__file__), "apache-ssl.cfg")
29+
30+
def __init__(self, buildout, name, options):
31+
self.name = name
32+
self.options = options
33+
self.buildout = buildout
34+
if "sslcert" in options:
35+
default_template = "apache-ssl.cfg"
36+
else:
37+
default_template = "apache.cfg"
38+
self.outputdir = os.path.join(self.buildout['buildout']['directory'], self.name)
39+
self.options.setdefault("template", sibpath(default_template))
40+
self.options.setdefault("passwdfile", os.path.join(self.outputdir, "passwd"))
41+
self.options.setdefault("configfile", os.path.join(self.outputdir, "apache.cfg"))
42+
self.options.setdefault("portal", "portal")
43+
44+
def install(self):
45+
if not os.path.isdir(self.outputdir):
46+
os.mkdir(self.outputdir)
47+
opt = self.options.copy()
48+
# turn a list of sslca's into an actual list
49+
opt['sslca'] = [x.strip() for x in opt.get("sslca", "").strip().split()]
50+
template = open(self.options['template']).read()
51+
cfgfilename = self.options['configfile']
52+
c = Template(template, searchList = opt)
53+
open(cfgfilename, "w").write(str(c))
54+
self.mkpasswd()
55+
return [self.outputdir]
56+
57+
def mkpasswd(self):
58+
if "password" in self.options:
59+
pw = htpasswd.HtpasswdFile(self.options['passwdfile'], create=True)
60+
pw.update(self.options["username"], self.options["password"])
61+
pw.save()
62+
63+
def update(self):
64+
pass
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# vim: syntax=apache:
2+
3+
<VirtualHost $interface:80>
4+
ServerName ${sitename}
5+
ServerAdmin ${serveradmin}
6+
CustomLog /var/log/apache2/${sitename}-access.log combined
7+
ErrorLog /var/log/apache2/${sitename}-error.log
8+
ProxyRequests Off
9+
RewriteEngine On
10+
RewriteRule /(.*)$ https://${sitename}/$1 [R]
11+
</VirtualHost>
12+
13+
<VirtualHost $interface:443>
14+
ServerName ${sitename}
15+
ServerAdmin ${serveradmin}
16+
CustomLog /var/log/apache2/ssl-${sitename}-access.log combined
17+
ErrorLog /var/log/apache2/ssl-${sitename}-error.log
18+
19+
SSLEngine on
20+
SSLCipherSuite ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP:+eNULL
21+
SSLCertificateFile ${sslcert}
22+
SSLCertificateKeyFile ${sslkey}
23+
#for $a in $sslca
24+
SSLCACertificateFile ${a}
25+
#end for
26+
27+
#if $getVar('realm', None)
28+
<Location />
29+
Options Indexes FollowSymLinks MultiViews
30+
Order Allow,Deny
31+
allow from all
32+
AuthType Basic
33+
AuthName "${realm}"
34+
AuthUserFile ${passwdfile}
35+
Require user ${username}
36+
</Location>
37+
#end if
38+
39+
#if $getVar('allowpurge', None)
40+
<Location />
41+
<LimitExcept GET POST HEAD>
42+
Order Deny,Allow
43+
Deny from all
44+
#for $a in $allowpurge.split()
45+
Allow from $a
46+
#end for
47+
</LimitExcept>
48+
</Location>
49+
#end if
50+
51+
ProxyRequests Off
52+
ProxyPass / http://localhost:${proxyport}/VirtualHostBase/https/$sitename:443/${portal}/VirtualHostRoot/
53+
ProxyPreserveHost On
54+
<Proxy *>
55+
Allow from all
56+
</Proxy>
57+
</VirtualHost>
58+
59+
# conditional, include lines
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# vim: syntax=apache:
2+
3+
<VirtualHost $interface:80>
4+
ServerName ${sitename}
5+
ServerAdmin ${serveradmin}
6+
CustomLog /var/log/apache2/${sitename}-access.log combined
7+
ErrorLog /var/log/apache2/${sitename}-error.log
8+
9+
#if $getVar('realm', None)
10+
<Location />
11+
Options Indexes FollowSymLinks MultiViews
12+
Order Allow,Deny
13+
allow from all
14+
AuthType Basic
15+
AuthName "${realm}"
16+
AuthUserFile ${passwdfile}
17+
Require user ${username}
18+
</Location>
19+
#end if
20+
21+
#if $getVar('allowpurge', None)
22+
<Location />
23+
<LimitExcept GET POST HEAD>
24+
Order Deny,Allow
25+
Deny from all
26+
#for $a in $allowpurge.split()
27+
Allow from $a
28+
#end for
29+
</LimitExcept>
30+
</Location>
31+
#end if
32+
33+
34+
ProxyRequests Off
35+
ProxyPass / http://localhost:${proxyport}/VirtualHostBase/http/$sitename:80/${portal}/VirtualHostRoot/
36+
ProxyPreserveHost On
37+
<Proxy *>
38+
Allow from all
39+
</Proxy>
40+
</VirtualHost>
41+
42+
# conditional, include lines
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#!/usr/bin/python
2+
"""Replacement for htpasswd"""
3+
# Original author: Eli Carter
4+
5+
import os
6+
import sys
7+
import random
8+
from optparse import OptionParser
9+
10+
# We need a crypt module, but Windows doesn't have one by default. Try to find
11+
# one, and tell the user if we can't.
12+
try:
13+
import crypt
14+
except ImportError:
15+
try:
16+
import fcrypt as crypt
17+
except ImportError:
18+
sys.stderr.write("Cannot find a crypt module. "
19+
"Possibly http://carey.geek.nz/code/python-fcrypt/\n")
20+
sys.exit(1)
21+
22+
23+
def salt():
24+
"""Returns a string of 2 randome letters"""
25+
letters = 'abcdefghijklmnopqrstuvwxyz' \
26+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' \
27+
'0123456789/.'
28+
return random.choice(letters) + random.choice(letters)
29+
30+
31+
class HtpasswdFile:
32+
"""A class for manipulating htpasswd files."""
33+
34+
def __init__(self, filename, create=False):
35+
self.entries = []
36+
self.filename = filename
37+
if not create:
38+
if os.path.exists(self.filename):
39+
self.load()
40+
else:
41+
raise Exception("%s does not exist" % self.filename)
42+
43+
def load(self):
44+
"""Read the htpasswd file into memory."""
45+
lines = open(self.filename, 'r').readlines()
46+
self.entries = []
47+
for line in lines:
48+
username, pwhash = line.split(':')
49+
entry = [username, pwhash.rstrip()]
50+
self.entries.append(entry)
51+
52+
def save(self):
53+
"""Write the htpasswd file to disk"""
54+
open(self.filename, 'w').writelines(["%s:%s\n" % (entry[0], entry[1])
55+
for entry in self.entries])
56+
57+
def update(self, username, password):
58+
"""Replace the entry for the given user, or add it if new."""
59+
pwhash = crypt.crypt(password, salt())
60+
matching_entries = [entry for entry in self.entries
61+
if entry[0] == username]
62+
if matching_entries:
63+
matching_entries[0][1] = pwhash
64+
else:
65+
self.entries.append([username, pwhash])
66+
67+
def delete(self, username):
68+
"""Remove the entry for the given user."""
69+
self.entries = [entry for entry in self.entries
70+
if entry[0] != username]
71+
72+
73+
def main():
74+
"""%prog [-c] -b filename username password
75+
Create or update an htpasswd file"""
76+
# For now, we only care about the use cases that affect tests/functional.py
77+
parser = OptionParser(usage=main.__doc__)
78+
parser.add_option('-b', action='store_true', dest='batch', default=False,
79+
help='Batch mode; password is passed on the command line IN THE CLEAR.'
80+
)
81+
parser.add_option('-c', action='store_true', dest='create', default=False,
82+
help='Create a new htpasswd file, overwriting any existing file.')
83+
parser.add_option('-D', action='store_true', dest='delete_user',
84+
default=False, help='Remove the given user from the password file.')
85+
86+
options, args = parser.parse_args()
87+
88+
def syntax_error(msg):
89+
"""Utility function for displaying fatal error messages with usage
90+
help.
91+
"""
92+
sys.stderr.write("Syntax error: " + msg)
93+
sys.stderr.write(parser.get_usage())
94+
sys.exit(1)
95+
96+
if not options.batch:
97+
syntax_error("Only batch mode is supported\n")
98+
99+
# Non-option arguments
100+
if len(args) < 2:
101+
syntax_error("Insufficient number of arguments.\n")
102+
filename, username = args[:2]
103+
if options.delete_user:
104+
if len(args) != 2:
105+
syntax_error("Incorrect number of arguments.\n")
106+
password = None
107+
else:
108+
if len(args) != 3:
109+
syntax_error("Incorrect number of arguments.\n")
110+
password = args[2]
111+
112+
passwdfile = HtpasswdFile(filename, create=options.create)
113+
114+
if options.delete_user:
115+
passwdfile.delete(username)
116+
else:
117+
passwdfile.update(username, password)
118+
119+
passwdfile.save()
120+
121+
122+
if __name__ == '__main__':
123+
main()

0 commit comments

Comments
 (0)