├── COPYING ├── Changelog ├── MANIFEST.in ├── README ├── StoneVPN ├── __init__.py └── app.py ├── TODO ├── bin └── stonevpn ├── conf └── stonevpn.conf ├── debian ├── changelog ├── compat ├── control ├── copyright └── rules ├── man ├── stonevpn.1 └── stonevpn.conf.5 ├── rpm └── stonevpn.spec └── setup.py /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 675 Mass Ave, Cambridge, MA 02139, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | Appendix: How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) 19yy name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2014-03-13 Léon Keijser 0.4.15 2 | * Added: support for Android. Patch by Nerijus Baliunas 4 | * Changed: version 0.4.15 5 | 6 | 2014-01-08 Léon Keijser 0.4.14 7 | * Fixed: bug with empty lines in openvpn config file. Fix 8 | by Julian Golderer 9 | * Changed: version 0.4.14 10 | 11 | 2012-11-09 Léon Keijser 0.4.13 12 | * Added: option to generate self-signed CA certificate 13 | * Fixed: assume SHA1 for default_md if key is missing in 14 | OpenSSL config file 15 | * Changed: version 0.4.13 16 | 17 | 2012-01-27 Léon Keijser 0.4.12 18 | * Fixed: empty index and serial file caused traceback. Fix 19 | by Jasper Capel 20 | * Changed: version 0.4.12 21 | 22 | 2011-03-24 Léon Keijser 0.4.11 23 | * Added: display expired certificate status 24 | * Fixed: remove old files before creating new certificate 25 | * Fixed: when revoking a cert, indexdb wasn't updated when a 26 | lower-case serial was provided 27 | * Changed: better output when displaying all certificates 28 | * Changed: version 0.4.11 29 | 30 | 2010-11-24 Léon Keijser 0.4.10 31 | * Added: option to generate random passphrase 32 | * Added: option to include the (generated) passphrase in 33 | the body of the e-mail. This option (wether used or not) will 34 | require the configuration file to be modified. 35 | * Fixed: check for the existance of the OpenSSL config file 36 | * Changed: version 0.4.10 37 | 38 | 2010-06-24 Léon Keijser 0.4.9 39 | * Added: option to generate configfiles for all OS's at once 40 | * Added: option to include extra files (eg. documentation) 41 | * Added: option to override server IP in configfile 42 | * Fixed: incorrectly writing serial number to serialfile 43 | * Fixed: check for empty variables in openssl configfile 44 | * Fixed: corrected Mac template after testing 45 | * Changed: passphrase can now be specified on the cmdline 46 | * Changed: version 0.4.9 47 | 48 | 2010-05-10 Léon Keijser 0.4.8.1 49 | * Fixed: incorrect exception handling on python < 2.6 50 | 51 | 2010-05-06 Léon Keijser 0.4.8 52 | * Added: client template for Mac (OSX) - UNTESTED 53 | * Added: now accepts adding multiple routes to clientfile 54 | * Added: debug option (mainly for my own benefit) 55 | * Added: expanded expiration date option to specify hours, 56 | days or years 57 | * Fixed: certificates were sometimes generated with an in- 58 | correct time, depending on local timezone. 59 | * Fixed: rewrote 'route' section 60 | * Fixed: freeip routine major rewrite. Will now also accept 61 | a broader netmask than /24 (or higher). 62 | * Fixed: crash when providing empty prefix 63 | * Changed: small cosmetic changes 64 | * Changed: version 0.4.8 65 | 66 | 2010-03-12 Léon Keijser 0.4.7 67 | * Fixed: certificates were generated with an incorrect 68 | serial number (hex was written as decimal) 69 | * Fixed: bug when listing all revoked certificates 70 | * Fixed: check for existence of crlfile before trying to 71 | parse it. 72 | * Added: new option to generate an empty CRL file 73 | * Added: grouped all options to make --help output look better 74 | * Changed: version 0.4.7 75 | 76 | 2010-02-19 Léon Keijser 0.4.6 77 | * Fixed: crash when trying to parse an empty CRL file 78 | * Fixed: indexdb wasn't closed after writing to it 79 | * Added: now prompts twice for password to verify 80 | * Changed: version 0.4.6 81 | 82 | 2010-02-02 Léon Keijser 0.4.5 83 | * Added: manpages for stonevpn (1) and stonevpn.conf (5) 84 | * Added: option for custom expiration date of certificate 85 | * Fixed: crash when reading an incorrectly written index.txt 86 | * Changed: version 0.4.5 87 | 88 | 2009-11-09 Léon Keijser 0.4.4 89 | * Added: debian files so StoneVPN can be debianized :) 90 | * Fixed: warning msg in freeip routine instead of exception 91 | * Fixed: probable incorrect path to cert/key files 92 | * Changed: install doc/example files seperately using 'install_docs' 93 | * Changed: updated spec file 94 | * Changed: version 0.4.4 95 | 96 | 2009-10-20 Léon Keijser 0.4.3 97 | * Changed: new patch to add CRL support in favor of old 98 | methods since upstream has already added PKCS12 support 99 | and will likely accept this patch as well. Code adapted. 100 | * Added: method to display information in CRL file 101 | * Changed: version 0.4.3 102 | 103 | 2009-08-07 Léon Keijser 104 | * Added: implemented nice python setup.py to install 105 | all the necessary files. 106 | * Updated: README file according to new install method 107 | 108 | 2009-05-19 Léon Keijser 109 | * Fixed: segfault when CRL didn't exist 110 | * Added: option to push extra route to client (stored in 111 | ccd dir on server) 112 | 113 | 2009-04-15 Léon Keijser 114 | 115 | * Fixed: minor typo: password -> passphrase 116 | 117 | 2009-03-27 Léon Keijser 118 | 119 | * Added: stonevpn.spec SPEC file 120 | * Updated: README file with instructions to build an RPM 121 | 122 | 2009-03-26 Léon Keijser 123 | 124 | * Added: copyright stuff 125 | * Added: modified pyOpenSSL-0.8-crl.patch 126 | * Added: README file 127 | * Added: TODO file 128 | 129 | 2009-03-23 Léon Keijser 130 | 131 | * Added: e-mail support 132 | 133 | 2009-03-05 Léon Keijser 134 | 135 | * Fixed: file prefix when set manually. 136 | 137 | 2009-03-02 Léon Keijser 138 | 139 | * Fixed: removed crypto CRL function from listAllCerts 140 | routine since we're not gonna use it anyway. 141 | 142 | * Fixed: when writing to indexdb, convert spaces to underscores 143 | in CNAME 144 | 145 | 2009-02-27 Léon Keijser 146 | 147 | * Fixed: made '--password' parameter work correctly 148 | 149 | 2009-02-20 Léon Keijser 150 | 151 | * Fixed: made KeyError messages a bit more specific when checking 152 | for required sections in OpenSSL configuration file. 153 | 154 | 2009-02-20 Léon Keijser 155 | 156 | * Added: option to print info about certificate 157 | 158 | 2009-02-19 Léon Keijser 159 | 160 | * Added: initial commit (version 0.3.3) 161 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include COPYING Changelog README TODO bin/stonevpn 2 | recursive-include conf * 3 | recursive-include rpm * 4 | recursive-include StoneVPN * 5 | recursive-include debian * 6 | recursive-include man * 7 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Description = 2 | StoneVPN - Easy OpenVPN certificate and configuration management 3 | 4 | Author: Léon Keijser - keijser@stone-it.com 5 | 6 | 7 | = Installation = 8 | In CentOS / RedHat / Fedora: 9 | 10 | StoneVPN is included in EPEL5 and Fedora 11 and later. Just use yum to install: 11 | 12 | $ yum install stonevpn 13 | 14 | 15 | From the tarball: 16 | 17 | * install these packages: 18 | python-configob 19 | python-ipy 20 | 21 | * run 'python setup.py install' as root 22 | 23 | * run 'python setup.py install_docs' to install example config and 24 | documentation in /usr/share/StoneVPN 25 | 26 | * copy /usr/share/StoneVPN/example/stonevpn.conf to /etc 27 | 28 | * Make the necessary adjustments in /etc/stonevpn.conf 29 | 30 | * Make the necessary adjustments in your openssl.cnf (StoneVPN 31 | should say what they are) 32 | 33 | * Optional: download latest pyOpenSSL to have CRL support (see below) 34 | 35 | * Run stonevpn :) 36 | 37 | 38 | = Custom pyOpenSSL = 39 | For StoneVPN to work optimally, you will need to download pyOpenSSL version 40 | 0.11 or later. Get it at https://launchpad.net/pyopenssl/+download 41 | 42 | 43 | = RPM = 44 | StoneVPN is now included in EPEL and Fedora so following these instructions 45 | are only for those who use different distro's: 46 | 47 | I've included a SPEC file so you can build an RPM yourself. To 48 | do this i assume you have a working rpmbuild environment and have 49 | the StoneVPN tarball located in ~/rpmbuild/SOURCES/stonevpn-%version.tar.gz 50 | 51 | For example 52 | 53 | $ mv /usr/share/StoneVPN/rpm/stonevpn.spec ~/rpmbuild/SPECS 54 | 55 | Build the RPMs: 56 | 57 | $ cd ~/rpmbuild/SPECS 58 | $ rpmbuild -ba stonevpn.spec 59 | 60 | If all goes well this will leave you with a stonevpn-%version.el5.noarch.rpm 61 | file which you can then use to install on various machines :) 62 | 63 | = Lastly = 64 | If you are actively using StoneVPN, drop me an email to let me know what 65 | you think of it. Or better yet, if you use it and like it enough to spend 66 | some money on it, please consider making a small donation: http://gotlinux.nl/stonevpn 67 | -------------------------------------------------------------------------------- /StoneVPN/__init__.py: -------------------------------------------------------------------------------- 1 | STONEVPN_VERSION = '0.4.16' 2 | -------------------------------------------------------------------------------- /StoneVPN/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | StoneVPN - Easy OpenVPN certificate and configuration management 4 | 5 | (C) 2009-2012 by L.S. Keijser, 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program; if not, write to the Free Software 19 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 20 | 21 | """ 22 | 23 | import commands 24 | import cStringIO 25 | import fileinput 26 | import getpass 27 | import glob 28 | import operator 29 | import os 30 | import random 31 | import re 32 | import shutil 33 | import smtplib 34 | import string 35 | import sys 36 | import time 37 | import zipfile 38 | from OpenSSL import SSL, crypto 39 | from optparse import OptionParser, OptionGroup 40 | from configobj import ConfigObj 41 | from time import strftime 42 | from datetime import date, datetime, timedelta 43 | from IPy import IP 44 | from string import atoi 45 | from datetime import datetime 46 | from email.MIMEMultipart import MIMEMultipart 47 | from email.MIMEBase import MIMEBase 48 | from email.MIMEText import MIMEText 49 | from email.Utils import formatdate 50 | from email import Encoders 51 | from socket import gethostname 52 | from StoneVPN import STONEVPN_VERSION 53 | 54 | 55 | def main(): 56 | stonevpnconf = '/etc/stonevpn.conf' 57 | stonevpnver = STONEVPN_VERSION 58 | # Read main configuration from stonevpn.conf 59 | if os.path.exists(stonevpnconf): 60 | config = ConfigObj(stonevpnconf) 61 | sectionname = 'stonevpn conf' 62 | section=config[sectionname] 63 | 64 | crlfile = section['crlfile'] 65 | prefix = section['prefix'] 66 | pushrouter = section['pushrouter'] 67 | cacertfile = section['cacertfile'] 68 | cakeyfile = section['cakeyfile'] 69 | openvpnconf = section['openvpnconf'] 70 | ccddir = section['ccddir'] 71 | working = section['working'] 72 | opensslconf = section['opensslconf'] 73 | ciphermethod = section['cipher'] 74 | mail_server = section['mail_server'] 75 | mail_cc = section['mail_cc'] 76 | mail_msg = section['mail_msg'] 77 | mail_from = section['mail_from'] 78 | try: 79 | mail_passtxt = section['mail_passtxt'] 80 | except: 81 | print "Missing variable 'mail_passtxt' in %s! Please update your configuration.\nHint: look at the example configuration file." % stonevpnconf 82 | sys.exit() 83 | else: 84 | print "File " + stonevpnconf + " does not exist!" 85 | sys.exit() 86 | 87 | # retrieve default expiration date from openssl.cnf, needed for optionparse 88 | if os.path.exists(opensslconf): 89 | config = ConfigObj(opensslconf) 90 | sectionname = 'CA_default' 91 | section=config[sectionname] 92 | defaultDays = section['default_days'] 93 | else: 94 | print "Error: OpenSSL configuration file not found at %s" % opensslconf 95 | sys.exit() 96 | 97 | # define some crypto stuff 98 | TYPE_RSA = crypto.TYPE_RSA 99 | TYPE_DSA = crypto.TYPE_DSA 100 | FILETYPE = crypto.FILETYPE_PEM 101 | 102 | # command line options 103 | parser = OptionParser(usage="%prog -f -n [ OPTIONS ]",version="%prog " + stonevpnver) 104 | 105 | # define groups 106 | group_crl = OptionGroup(parser, "Certificate revocation options") 107 | group_general = OptionGroup(parser, "General options", 108 | "All general options are mandatory") 109 | group_extra = OptionGroup(parser, "Extra options", 110 | "To be used in conjunction with the general options.") 111 | group_info = OptionGroup(parser, "Information/printing options") 112 | group_ca = OptionGroup(parser, "CA certificate options") 113 | group_test = OptionGroup(parser, "Test/experimental options", 114 | "Caution: use these options with care.") 115 | 116 | # define special case for action with optional argument 117 | def optional_arg(arg_default): 118 | def check_value(option,opt_str,value,parser): 119 | # check for remaining args. these shouldn't start with a '-' 120 | if parser.rargs and not parser.rargs[0].startswith('-'): 121 | val=parser.rargs[0] 122 | parser.rargs.pop(0) 123 | else: 124 | # return the default value for the argument 125 | val=arg_default 126 | # remove the argument from the list and return the remaining args back to parser 127 | setattr(parser.values,option.dest,val) 128 | return check_value 129 | 130 | # populate groups 131 | parser.add_option("-D", "--debug", 132 | action="count", 133 | dest="debug", 134 | help="enable debugging output") 135 | group_general.add_option("-n", "--name", 136 | action="store", 137 | type="string", 138 | dest="cname", 139 | help="Common Name, use quotes eg.: \"CNAME\" and only alphanumeric characters") 140 | group_general.add_option("-f", "--file", 141 | dest="fname", 142 | help="write to file FNAME (no extension!)") 143 | group_general.add_option("-o", "--config", 144 | action="store", 145 | dest="confs", 146 | default="unix", 147 | help="create config files for [windows|unix|mac|android|all]") 148 | group_extra.add_option("-e", "--prefix", 149 | action="store", 150 | dest="fprefix", 151 | default=prefix, 152 | help="prefix (almost all) generated files. Default = " + str(prefix)) 153 | group_extra.add_option("-z", "--zip", 154 | action="store_true", 155 | dest="zip", 156 | help="add all generated files to a ZIP-file") 157 | group_extra.add_option("-m", "--mail", 158 | action="store", 159 | type="string", 160 | dest="emailaddress", 161 | help="send all generated files to EMAILADDRESS") 162 | group_extra.add_option("-i", "--free-ip", 163 | action="store_true", 164 | dest="freeip", 165 | help="locate and assign free ip") 166 | group_extra.add_option("-E", "--extrafile", 167 | action="append", 168 | dest="extrafile", 169 | help="include extra file(s) like documentation. Can be used multiple times") 170 | group_extra.add_option("-p", "--passphrase", 171 | action="callback", 172 | callback=optional_arg('please_prompt_me'), 173 | dest="passphrase", 174 | help="prompt for a passphrase when generating private key, or supply one on the commandline") 175 | group_extra.add_option("-M", "--mailpass", 176 | action="store_true", 177 | dest="mailpass", 178 | help="include passphrase in e-mail body (only useful with the '-m' option)") 179 | group_extra.add_option("-R", "--randpass", 180 | action="store", 181 | type="string", 182 | dest="randpass", 183 | help="generate a random password of RANDPASS characters (eg.: -R 8)") 184 | group_extra.add_option("-S", "--serverip", 185 | action="store", 186 | type="string", 187 | dest="server_ip", 188 | help="use this IP address for the server when generating the configuration file, overriding the one specified in stonevpn.conf") 189 | group_crl.add_option("-r", "--revoke", 190 | action="store", 191 | dest="serial", 192 | help="revoke certificate with serial SERIAL") 193 | group_extra.add_option("-u", "--route", 194 | action="append", 195 | dest="route", 196 | help="push extra route(s) to client. Specify multiple routes as: -u 192.168.1.1/32 -u 10.1.4.0/24") 197 | group_crl.add_option("-l", "--listrevoked", 198 | action="store_true", 199 | dest="listrevoked", 200 | help="list revoked certificates") 201 | group_crl.add_option("-C", "--crl", 202 | action="store_true", 203 | dest="displaycrl", 204 | help="display CRL file contents") 205 | group_info.add_option("-a", "--listall", 206 | action="store_true", 207 | dest="listall", 208 | help="list all certificates") 209 | group_info.add_option("--listcsv", 210 | action="store_true", 211 | dest="listallcsv", 212 | help="list all certificates (output as CSV)") 213 | group_info.add_option("-s", "--showserial", 214 | action="store_true", 215 | dest="showserial", 216 | help="display current SSL serial number") 217 | group_info.add_option("-c", "--printcert", 218 | action="store", 219 | dest="printcert", 220 | help="prints information about a certficiate file") 221 | group_info.add_option("-d", "--printindex", 222 | action="store_true", 223 | dest="printindex", 224 | help="prints index file") 225 | group_extra.add_option("-x", "--expire", 226 | action="store", 227 | dest="expiredate", 228 | help="certificate expires in EXPIREDATE h(ours), d(ays) or y(ears). The default is " + str(defaultDays) + " days. Example usage: -x 2h") 229 | group_crl.add_option("-N", "--newcrl", 230 | action="store_true", 231 | dest="emptycrl", 232 | help="create an empty CRL file (or overwrite an existing one)") 233 | group_ca.add_option("-A", "--newca", 234 | action="store", 235 | dest="newca", 236 | help="create a new self-signed CA certificate that's valid for NEWCA years") 237 | group_test.add_option("-t", "--test", 238 | action="store_true", 239 | dest="test", 240 | help="Danger, Will Robinson, Danger! test parameter - can do anything! Review source before executing!") 241 | 242 | # add optiongroups 243 | parser.add_option_group(group_general) 244 | parser.add_option_group(group_extra) 245 | parser.add_option_group(group_info) 246 | parser.add_option_group(group_crl) 247 | parser.add_option_group(group_ca) 248 | parser.add_option_group(group_test) 249 | 250 | # parse cmd line options 251 | (options, args) = parser.parse_args() 252 | 253 | s = StoneVPN() 254 | # values we got from optparse: 255 | s.debug = options.debug 256 | s.cname = options.cname 257 | s.fname = options.fname 258 | s.confs = options.confs 259 | s.fprefix = options.fprefix 260 | s.zip = options.zip 261 | s.emailaddress = options.emailaddress 262 | s.freeip = options.freeip 263 | s.passphrase = options.passphrase 264 | s.mailpass = options.mailpass 265 | s.randpass = options.randpass 266 | s.extrafile = options.extrafile 267 | s.server_ip = options.server_ip 268 | s.serial = options.serial 269 | s.route = options.route 270 | s.listrevoked = options.listrevoked 271 | s.displaycrl = options.displaycrl 272 | s.listall = options.listall 273 | s.listallcsv = options.listallcsv 274 | s.showserial = options.showserial 275 | s.printcert = options.printcert 276 | s.printindex = options.printindex 277 | s.expiredate = options.expiredate 278 | s.emptycrl = options.emptycrl 279 | s.newca = options.newca 280 | s.test = options.test 281 | # values we got from parsing the configuration file: 282 | s.cacertfile = cacertfile 283 | s.cakeyfile = cakeyfile 284 | s.openvpnconf = openvpnconf 285 | s.ccddir = ccddir 286 | s.working = working 287 | s.opensslconf = opensslconf 288 | s.pushrouter = pushrouter 289 | s.ciphermethod = ciphermethod 290 | s.prefix = prefix 291 | s.crlfile = crlfile 292 | s.mail_server = mail_server 293 | s.mail_cc = mail_cc 294 | s.mail_msg = mail_msg 295 | s.mail_from = mail_from 296 | s.mail_passtxt = mail_passtxt 297 | s.stonevpnconf = stonevpnconf 298 | # and all other variables 299 | s.TYPE_RSA = TYPE_RSA 300 | s.TYPE_DSA = TYPE_DSA 301 | s.FILETYPE = FILETYPE 302 | s.stonevpnver = stonevpnver 303 | 304 | # check for all args 305 | if len(sys.argv[1:]) == 0: 306 | parser.print_help() 307 | 308 | # check for valid args 309 | if options.fname is None and options.serial is not None and options.listrevoked is not None and options.listall is not None and options.listallcsv is not None and options.showserial is not None and options.printcert is not None and options.printindex is not None and options.emptycrl is not None and options.test is not None and options.newca is not None: 310 | parser.error("Error: you have to specify a filename (FNAME)") 311 | else: 312 | # must..have..root.. 313 | myId = commands.getstatusoutput('id -u')[1] 314 | if not myId == '0': 315 | print "Sorry, root privileges required for this action." 316 | sys.exit(0) 317 | else: 318 | s.run() 319 | 320 | class StoneVPN: 321 | 322 | def __init__(self): 323 | """ 324 | Constructor. Arguments will be filled in by optparse.. 325 | """ 326 | self.cname = None 327 | self.fname = None 328 | self.confs = None 329 | self.fprefix = None 330 | self.zip = None 331 | self.emailaddress = None 332 | self.freeip = None 333 | self.passphrase = None 334 | self.mailpass = None 335 | self.randpass = None 336 | self.extrafile = None 337 | self.server_ip = None 338 | self.serial = None 339 | self.route = None 340 | self.listrevoked = None 341 | self.displaycrl = None 342 | self.listall = None 343 | self.listallcsv = None 344 | self.showserial = None 345 | self.printcert = None 346 | self.printindex = None 347 | self.expiredate = None 348 | self.emptycrl = None 349 | self.newca = None 350 | self.test = None 351 | 352 | # Read certain vars from OpenSSL config file 353 | def readOpenSSLConf(self): 354 | if self.debug: print "DEBUG: parsing OpenSSL configuration file %s" % self.opensslconf 355 | config = ConfigObj(self.opensslconf) 356 | sectionname = 'req_distinguished_name' 357 | section=config[sectionname] 358 | # make these variables also global 359 | global countryName, stateOrProvinceName, localityName, organizationName, organizationalUnitName, defaultDays, prefixdir, indexdb, serialfile, default_bits, default_md 360 | # Check if certain sections in OpenSSL configfile are present, report if they're not 361 | try: 362 | countryName = section['countryName_default'] 363 | if len(countryName) is 0: 364 | print "Error: countryName_default is empty. Please edit %s first." % self.opensslconf 365 | sys.exit() 366 | except KeyError: 367 | print "Error: missing section 'countryName_default' in " + self.opensslconf 368 | sys.exit() 369 | try: 370 | stateOrProvinceName = section['stateOrProvinceName_default'] 371 | if len(stateOrProvinceName) is 0: 372 | print "Error: stateOrProvinceName_default is empty. Please edit %s first." % self.opensslconf 373 | sys.exit() 374 | except KeyError: 375 | print "Error: missing section 'stateOrProvinceName_default' in " + self.opensslconf 376 | sys.exit() 377 | try: 378 | localityName = section['localityName_default'] 379 | if len(localityName) is 0: 380 | print "Error: localityName_default is empty. Please edit %s first." % self.opensslconf 381 | sys.exit() 382 | except KeyError: 383 | print "Error: missing section 'localityName_default' in " + self.opensslconf 384 | sys.exit() 385 | try: 386 | organizationName = section['0.organizationName_default'] 387 | if len(organizationName) is 0: 388 | print "Error: 0.organizationName_default is empty. Please edit %s first." % self.opensslconf 389 | sys.exit() 390 | except KeyError: 391 | print "Error: missing section '0.organizationName_default' in " + self.opensslconf 392 | sys.exit() 393 | try: 394 | organizationalUnitName = section['organizationalUnitName_default'] 395 | if len(organizationalUnitName) is 0: 396 | print "Error: organizationalUnitName_default is empty. Please edit %s first." % self.opensslconf 397 | sys.exit() 398 | except KeyError: 399 | print "Error: missing section 'organizationalUnitName_default' in " + self.opensslconf 400 | sys.exit() 401 | sectionname = 'CA_default' 402 | section=config[sectionname] 403 | defaultDays = section['default_days'] 404 | prefixdir = section['dir'] 405 | indexdb = section['database'].replace('$dir', prefixdir) 406 | serialfile = section['serial'].replace('$dir', prefixdir) 407 | sectionname = 'req' 408 | section=config[sectionname] 409 | default_bits = section['default_bits'] 410 | try: 411 | default_md = section['default_md'] 412 | except KeyError: 413 | print "Warning: your OpenSSL configuration file misses key 'default_md'. Please add it. For now we'll assume SHA1'" 414 | default_md = 'sha1' 415 | 416 | def run(self): 417 | """ 418 | StoneVPN's main function 419 | """ 420 | 421 | if os.path.exists(self.opensslconf): 422 | self.readOpenSSLConf() 423 | else: 424 | print "File " + self.opensslconf + " does not exist!" 425 | sys.exit() 426 | 427 | # Check for presence of OpenSSL index file 428 | if not os.path.exists(indexdb): 429 | print "Error: indexfile not found at: " + indexdb + " or insufficient rights." 430 | sys.exit() 431 | 432 | # Check for presence of OpenSSL serial file 433 | if not os.path.exists(serialfile): 434 | print "Error: serialfile not found at: " + serialfile + " or insufficient rights." 435 | sys.exit() 436 | 437 | # Make sure FPREFIX ends with a dash 438 | if not self.fprefix == '': 439 | if not self.fprefix[-1] == '-': 440 | self.fprefix = str(self.fprefix) + '-' 441 | 442 | # check if working dir exists, create it if it doesn't 443 | if not os.path.exists(self.working): 444 | print "Working dir didn't exist, making ..." 445 | os.mkdir(self.working) 446 | # Make certificates 447 | if self.cname: 448 | if self.fname is None: 449 | print "Error: required option -f/--file is missing." 450 | sys.exit() 451 | print "Creating " + self.fname + ".key and " + self.fname + ".crt for " + self.cname 452 | self.makeCert( self.fname, self.cname ) 453 | 454 | # check for extra files to be included 455 | if self.extrafile: 456 | if self.fname is None or self.cname is None: 457 | print "Error: required option -f/--file and/or -n/--name is missing." 458 | sys.exit() 459 | for efile in self.extrafile: 460 | if os.path.exists(efile): 461 | # copy them to a temp subdir within the working dir to avoid duplicates 462 | try: 463 | os.mkdir(self.working + '/' + self.fname + '-extrafiles') 464 | except: 465 | pass 466 | print "Adding extra file %s" % efile 467 | shutil.copy(efile, self.working + '/' + self.fname + '-extrafiles/') 468 | else: 469 | # exit if the file wasn't found 470 | print "Error: file %s was not found." 471 | sys.exit() 472 | 473 | # Make nice zipfile from all the generated files 474 | # :: called only when option '-z' is used :: 475 | if self.zip: 476 | if self.fname is None or self.cname is None: 477 | print "Error: required option -f/--file and/or -n/--name is missing." 478 | sys.exit() 479 | print "Adding all files to " + self.working + "/" + self.fprefix + self.fname + ".zip" 480 | z = zipfile.ZipFile(self.working + "/" + self.fprefix + self.fname + ".zip", "w") 481 | for name in glob.glob(self.working + "/" + self.fprefix + self.fname + ".*"): 482 | # only add the files that begin with the name specified with the -f option, don't add the zipfile itself (duh) 483 | if not name == self.working + "/" + self.fprefix + self.fname + ".zip": 484 | if self.debug: print "DEBUG: adding %s to %s" % (name.split('/')[-1],self.fprefix + self.fname + '.zip') 485 | z.write(name, os.path.basename(name), zipfile.ZIP_DEFLATED) 486 | # and add the CA certificate file 487 | z.write(self.cacertfile, os.path.basename(self.cacertfile), zipfile.ZIP_DEFLATED) 488 | # check if extra files should be included as well 489 | if self.extrafile: 490 | for efile in self.extrafile: 491 | z.write(efile, os.path.basename(efile), zipfile.ZIP_DEFLATED) 492 | # we can safely remove all files in the temp dir now since it was only used when not creating a zip file 493 | for file in glob.glob(self.working + "/" + self.fname + "-extrafiles/*"): 494 | os.remove(file) 495 | # finally remove the temp dir itself 496 | os.rmdir(self.working + "/" + self.fname + "-extrafiles") 497 | z.close() 498 | # delete all the files generated, except the ZIP-file 499 | for file in glob.glob(self.working + "/" + self.fprefix + self.fname + ".*"): 500 | if not file == self.working + "/" + self.fprefix + self.fname + ".zip": os.remove(file) 501 | 502 | # Find free IP-address by parsing config files (usually in /etc/openvpn/ccd/*) 503 | # :: called only when option '-i' is used :: 504 | if self.freeip: 505 | if self.fname is None: 506 | print "Error: required option -f/--file is missing." 507 | sys.exit() 508 | print "Searching for free IP-address:" 509 | # see if vpn server conf file exists 510 | if not os.path.exists(self.openvpnconf): 511 | print "Error: OpenVPN server configuration file was not found at %s" % self.openvpnconf 512 | sys.exit() 513 | # parse config file in search for ifconfig-pool 514 | for line in fileinput.input(self.openvpnconf): 515 | if len(line.split()) > 0 and line.split()[0] == 'ifconfig-pool': 516 | pool_from = line.split()[1] 517 | pool_to = line.split()[2] 518 | print "Pool runs from " + pool_from + " to " + pool_to 519 | # from here on we have to do some magic to get a list of 520 | # valid IP's in the specified pool 521 | # we first check if the first 3 octets in both 'from' and 'to' 522 | # are the same. 523 | r = re.compile('(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})') 524 | mFrom = r.match(pool_from) 525 | mTo = r.match(pool_to) 526 | if self.debug: 527 | print "DEBUG: first 3 octets of ip_from are %s.%s.%s" % (mFrom.group(1),mFrom.group(2),mFrom.group(3)) 528 | print "DEBUG: first 3 octets of ip_to are %s.%s.%s" % (mTo.group(1),mTo.group(2),mTo.group(3)) 529 | from_3octs = str(mFrom.group(1)) + '.' + str(mFrom.group(2)) + '.' + str(mFrom.group(3)) 530 | to_3octs = str(mTo.group(1)) + '.' + str(mTo.group(2)) + '.' + str(mTo.group(3)) 531 | if from_3octs == to_3octs: 532 | # ip's in pool are in a /24 (or higher, thus less addresses) subnet 533 | # create a range of valid ip's addresses and put them in a list 534 | if self.debug: print "DEBUG: both ip_from and ip_to are in the same subnet\nDEBUG: calculating range the 'easy' way.." 535 | range_4oct = range(int(mFrom.group(4)),int(mTo.group(4))) 536 | # fill range with ip's 537 | rangeIP = [] 538 | for octet in range_4oct: 539 | rangeIP.append(from_3octs + "." + str(octet)) 540 | else: 541 | rangeIP = [] 542 | # ip's in pool are not in a /24 subnet 543 | # we'll have to manually (well, kind of) calculate the range. 544 | range_3oct = range(int(mFrom.group(3)),int(mTo.group(3))) 545 | # append last octet since range() doesn't do that 546 | range_3oct.append(int(mTo.group(3))) 547 | if self.debug: print "DEBUG: range_3oct is %s" % range_3oct 548 | # the first element in the range is the starting octet and thus 549 | # we should look at the 4th octet now to determine the starting point 550 | for octet in range(int(mFrom.group(4)),255): 551 | # fill rangeIP with valid ip's 552 | rangeIP.append(from_3octs + "." + str(octet)) 553 | # now remove the first octet from the range 554 | if self.debug: print "DEBUG: remove %s from %s" % (mFrom.group(3),range_3oct) 555 | range_3oct.remove(int(mFrom.group(3))) 556 | if self.debug: print "DEBUG: range_3oct is now %s" % range_3oct 557 | # now iterate over the rest, until we get to the last (3rd) octet 558 | for octet_3 in range_3oct: 559 | if int(octet_3) != int(mTo.group(3)): 560 | for octet in range(1,255): 561 | rangeIP.append(mFrom.group(1) + '.' + mFrom.group(2) + '.' + str(octet_3) + '.' + str(octet)) 562 | else: 563 | # this is the last (3rd) octet so only fill the list until the 4th octet of pool_to 564 | for octet in range(1,int(mTo.group(4))): 565 | rangeIP.append(mFrom.group(1) + '.' + mFrom.group(2) + '.' + str(mTo.group(3)) + '.' + str(octet)) 566 | if self.debug: print "DEBUG: rangeIP is %s" % rangeIP 567 | # define list of IP-addresses 568 | ipList = [] 569 | for x in rangeIP: 570 | ipList.append(x) 571 | # go through the individual config files to find IP-addresses 572 | for file in glob.glob(self.ccddir+"/*"): 573 | if self.debug: print "DEBUG: parsing file: " + file 574 | for line in fileinput.input(file): 575 | # search for line that starts with 'ifconfig-push' 576 | if line.split()[0] == 'ifconfig-push': 577 | # the client IP is the 2nd argument ([2] is 0,1,2nd object on the line) 578 | clientip = line.split()[2] 579 | # remove IP from range if it exists in the list 580 | if clientip in ipList: 581 | ipList.remove(clientip) 582 | # the server IP is the 1st argument 583 | servip = line.split()[1] 584 | # remove IP from range if it exists in the list 585 | if servip in ipList: 586 | ipList.remove(servip) 587 | # sort list 588 | ipList.sort() 589 | # we now have a list of usable IP addresses :) 590 | # find 2 free IP-addresses: 591 | try: 592 | firstFree = ipList[0] 593 | except IndexError: 594 | print "Error: no free IP address left in pool!" 595 | sys.exit() 596 | try: 597 | secondFree = ipList[1] 598 | except IndexError: 599 | print "Error: no free IP address left in pool!" 600 | sys.exit() 601 | print "First free address: %s (local)" % firstFree 602 | print "Second free address: %s (peer)" % secondFree 603 | # check if ccd dir exists: 604 | if not os.path.exists(self.ccddir): 605 | print "Client configuration directory didn't exist, making ..." 606 | os.mkdir(self.ccddir) 607 | # And create the configuration file for these addresses 608 | nospaces_cname = self.cname.replace(' ', '_') 609 | f=open(self.ccddir + '/' + nospaces_cname, 'w') 610 | f.write('ifconfig-push ' + str(secondFree) + ' ' + str(firstFree) + '\n') 611 | f.write('push "route ' + self.pushrouter + ' 255.255.255.255"\n') 612 | f.close() 613 | print "CCD file written to: %s\nPlease review or make additional changes." % (self.ccddir + '/' + nospaces_cname) 614 | 615 | if self.listall: 616 | self.listAllCerts() 617 | 618 | if self.listallcsv: 619 | self.listAllCertsCSV() 620 | 621 | if self.displaycrl: 622 | self.displayCRL() 623 | 624 | if self.listrevoked: 625 | self.listRevokedCerts() 626 | 627 | if self.serial: 628 | self.revokeCert(str(self.serial)) 629 | 630 | if self.showserial: 631 | print "Current SSL serial number (in hex): " + self.readSerial() 632 | 633 | if self.printindex: 634 | print "Current index file (" + indexdb + "):" 635 | self.printIndexDB() 636 | 637 | if self.printcert: 638 | self.print_cert ( self.printcert ) 639 | 640 | if self.emailaddress: 641 | if self.fname is None or self.cname is None: 642 | print "Error: required option -f/--file and/or -n/--name is missing." 643 | sys.exit() 644 | mail_attachment = [] 645 | mail_to = self.emailaddress 646 | # First check if we've generated a ZIP file (include just one attachment) or not (include all generated files) 647 | if self.zip: 648 | mail_attachment.append(self.working + '/' + self.fprefix + self.fname + '.zip') 649 | self.send_mail(self.mail_from, mail_to, 'StoneVPN: generated files for ' + str(self.cname), self.mail_msg, mail_attachment) 650 | else: 651 | # Generate a list of filenames to include as attachments 652 | for name in glob.glob(self.working + "/" + self.fprefix + self.fname + ".*"): 653 | mail_attachment.append(name) 654 | # Also include the CA certificate 655 | mail_attachment.append(self.cacertfile) 656 | # And check for extra files to be included 657 | if self.extrafile: 658 | for efile in self.extrafile: 659 | mail_attachment.append(efile) 660 | # Finally, send the mail 661 | self.send_mail(self.mail_from, mail_to, 'StoneVPN: generated files for ' + str(self.cname), self.mail_msg, mail_attachment) 662 | 663 | 664 | if self.route: 665 | if self.cname is None: 666 | print "Error: required option -n/--name is missing." 667 | sys.exit() 668 | IP.check_addr_prefixlen = False 669 | nospaces_cname = self.cname.replace(' ', '_') 670 | clientfile = self.ccddir + "/" + nospaces_cname 671 | # nowwhat : 0=continue normally (append), 1=don't write clientfile, 2=overwrite) 672 | # setting to 'append' by default 673 | nowwhat=0 674 | if not self.freeip: 675 | if os.path.exists(clientfile): 676 | overwrite=raw_input("Existing client configuration file was found. Do you want to (o)verwrite, (A)ppend or (s)kip): ") 677 | if overwrite in ('o', 'O'): 678 | os.remove(clientfile) 679 | nowwhat=2 680 | elif overwrite in ('s', 'S'): 681 | nowwhat=1 682 | if self.debug: print "DEBUG: adding %s routes" % len(self.route) 683 | for newroute in self.route: 684 | try: 685 | ip=IP(newroute) 686 | except ValueError: 687 | print "Error: invalid prefix length given." 688 | sys.exit() 689 | ip.NoPrefixForSingleIp = None 690 | ip.WantPrefixLen = 2 691 | if self.debug: print "DEBUG: ip: %s" % ip 692 | # check if supplied argument is an IPv4 address 693 | if IP(ip).version() != 4: 694 | print "Error: only IPv4 addresses are supported." 695 | sys.exit() 696 | route = str(ip).split('/') 697 | if self.debug: 698 | if len(route) == 1: 699 | print "DEBUG: only IP given, assume /32 netmask" 700 | # check if ccd dir exists: 701 | if not os.path.exists(self.ccddir): 702 | print "Client configuration directory didn't exist, making ..." 703 | os.mkdir(self.ccddir) 704 | f=open(self.ccddir + '/' + nospaces_cname, 'a') 705 | if self.debug: print "DEBUG: route: %s" % route 706 | if nowwhat == 1: 707 | if self.debug: print "DEBUG: not writing route to client configfile!" 708 | # only write routes if we didn't skip overwriting/appending earlier 709 | if nowwhat != 1: 710 | print "Adding route %s / %s" % (route[0],route[1]) 711 | f.write("push \"route " + route[0] + " " + route[1] + "\"\n") 712 | f.close() 713 | if nowwhat != 1: 714 | print "Wrote extra route(s) to " + self.ccddir + "/" + nospaces_cname 715 | 716 | if self.emptycrl: 717 | try: 718 | crl = crypto.CRL() 719 | except: 720 | print "\nError: CRL support is not available in your version of" 721 | print "pyOpenSSL. Please check the README file that came with" 722 | print "StoneVPN to see what you can do about this. For now, " 723 | print "you will have to revoke certificates manually.\n" 724 | sys.exit() 725 | if os.path.exists(self.crlfile): 726 | overwrite=raw_input("Existing crlfile was found. Do you want to overwrite (y/N): ") 727 | if overwrite not in ('y', 'Y'): 728 | print "Doing nothing.." 729 | sys.exit() 730 | print "Creating empty CRL file at %s" % self.crlfile 731 | cacert = self.load_cert(self.cacertfile) 732 | cakey = self.load_key(self.cakeyfile) 733 | newCRL = crl.export(cacert, cakey, days=90) 734 | f=open(self.crlfile, 'w') 735 | f.write(newCRL) 736 | f.close() 737 | 738 | if self.newca: 739 | self.createSelfSignedCertificate(self.newca) 740 | 741 | if self.test: 742 | print "Testing 1, 2, 5 ... three Sir!" 743 | sys.exit() 744 | 745 | # Create key 746 | def createKeyPair(self, type, bits): 747 | pkey = crypto.PKey() 748 | pkey.generate_key(type, bits) 749 | return pkey 750 | 751 | # Create request 752 | def createCertRequest(self, pkey, digest='sha256', **name): 753 | req = crypto.X509Req() 754 | subj = req.get_subject() 755 | for (key,value) in name.items(): 756 | setattr(subj, key, value) 757 | req.set_pubkey(pkey) 758 | req.sign(pkey, default_md) 759 | return req 760 | 761 | # decimal 2 hexidecimal and vice versa 762 | def dec2hex(self, n): 763 | return "%X" % n 764 | 765 | def hex2dec(self, s): 766 | return int(s, 16) 767 | 768 | def printIndexDB(self): 769 | f=open(indexdb, 'r') 770 | for line in f: 771 | print line 772 | f.close() 773 | 774 | def readSerial(self): 775 | f=open(serialfile, 'r') 776 | serial = f.readline() 777 | f.close() 778 | # see if we got more than just a newline. If not, return 0 779 | if not serial.strip(): 780 | serial = "0" 781 | return serial 782 | 783 | def writeSerial(self, serial): 784 | f=open(serialfile, 'w') 785 | f.write(serial) 786 | f.close() 787 | 788 | def writeIndex(self, index): 789 | f=open(indexdb, 'a') 790 | f.write(index) 791 | f.close() 792 | 793 | # Create certificate 794 | def createCertificate(self, req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest='sha256'): 795 | extensions = [] 796 | # Create the X509 Extensions 797 | extensions.append(crypto.X509Extension('basicConstraints',1, 'CA:FALSE')) 798 | try: 799 | extensions.append(crypto.X509Extension('nsComment',0, 'Created with stonevpn ' + str(self.stonevpnver))) 800 | except ValueError: 801 | print "\n==================================================================" 802 | print "Warning: your version of pyOpenSSL doesn't support X509Extensions." 803 | print "Please consult the README file that came with StoneVPN in order to" 804 | print "fix this. This is not trivial. The certificate will be generated." 805 | print "==================================================================\n" 806 | # We're creating a X509 certificate version 2 807 | cert = crypto.X509() 808 | cert.set_version ( 2 ) 809 | # Add the Extension to the certificate 810 | cert.add_extensions(extensions) 811 | # Create a valid hexidecimal serial number 812 | goodserial = atoi(str(serial), 16) 813 | cert.set_serial_number(goodserial) 814 | if self.debug: print "DEBUG: notBefore is %s, notAfter is %s" % (notBefore,notAfter) 815 | #cert.gmtime_adj_notBefore(notBefore) 816 | #cert.gmtime_adj_notAfter(notAfter) 817 | now = datetime.utcnow().strftime("%Y%m%d%H%M%SZ") 818 | if self.debug: print "DEBUG: days is %s" % timedelta(seconds=notAfter) 819 | expire = (datetime.utcnow() + timedelta(seconds=notAfter)).strftime("%Y%m%d%H%M%SZ") 820 | cert.set_notBefore(now) 821 | cert.set_notAfter(expire) 822 | cert.set_issuer(issuerCert.get_subject()) 823 | cert.set_subject(req.get_subject()) 824 | cert.set_pubkey(req.get_pubkey()) 825 | cert.sign(issuerKey, digest) 826 | return cert 827 | 828 | # Passphrase 829 | def getPass(self): 830 | passA = getpass.getpass('Enter passphrase for private key: ') 831 | passB = getpass.getpass('Enter passphrase for private key (again): ') 832 | if passA == passB: 833 | return passB 834 | else: 835 | print "Error: passwords don't match!" 836 | return "password_error" 837 | 838 | # Simple routines to load/save files using crypto lib 839 | # Save private key to file 840 | def save_key (self, fn, key): 841 | global keyPass 842 | # Adding passphrase to private key 843 | # do we need a random passphrase? 844 | if self.randpass: 845 | if self.debug: print "DEBUG: generating a random passphrase of %s characters" % self.randpass 846 | keyPass = "" 847 | for i in range(int(self.randpass)): 848 | keyPass += random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') 849 | fp = open ( fn, 'w' ) 850 | fp.write ( crypto.dump_privatekey ( self.FILETYPE, key, self.ciphermethod, keyPass ) ) 851 | print "Private key encrypted with RANDOM passphrase: '%s'" % keyPass 852 | elif self.passphrase: 853 | if self.passphrase == 'please_prompt_me': 854 | keyPass = self.getPass() 855 | if keyPass is "password_error": 856 | # Don't write keyfile if supplied passwords mismatch 857 | sys.exit() 858 | else: 859 | fp = open ( fn, 'w' ) 860 | fp.write ( crypto.dump_privatekey ( self.FILETYPE, key, self.ciphermethod, keyPass ) ) 861 | if self.debug: print "DEBUG: private key encrypted with passphrase: '%s'" % keyPass 862 | else: 863 | fp = open ( fn, 'w' ) 864 | fp.write ( crypto.dump_privatekey ( self.FILETYPE, key, self.ciphermethod, self.passphrase ) ) 865 | if self.debug: print "DEBUG: private key encrypted with passphrase: '%s'" % self.passphrase 866 | else: 867 | fp = open ( fn, 'w' ) 868 | fp.write ( crypto.dump_privatekey ( self.FILETYPE, key ) ) 869 | fp.close () 870 | 871 | # Save certificate to file 872 | def save_cert (self, fn, cert): 873 | fp = open ( fn, 'w' ) 874 | fp.write ( crypto.dump_certificate ( self.FILETYPE, cert ) ) 875 | fp.close () 876 | 877 | # Load private key from file 878 | def load_key (self, fn): 879 | fp = open ( fn, 'r' ) 880 | ret = crypto.load_privatekey ( self.FILETYPE, fp.read() ) 881 | fp.close () 882 | return ret 883 | 884 | # Load certificate from file 885 | def load_cert (self, fn): 886 | fp = open ( fn, 'r' ) 887 | ret = crypto.load_certificate ( self.FILETYPE, fp.read() ) 888 | fp.close () 889 | return ret 890 | 891 | # Print information retreived from a certificate file 892 | def print_cert (self, cert): 893 | try: 894 | certfile = self.load_cert( cert ) 895 | except: 896 | print "Error opening certificate file" 897 | sys.exit() 898 | # Some objects are 'X509Name objects' so we have to fiddle a bit to output to a human-readable format 899 | certIssuerArray = str(certfile.get_issuer()).replace('','').split('/') 900 | certIssuer = certIssuerArray[1] + ', ' + certIssuerArray[2] + ', ' + certIssuerArray[3] + ', ' + certIssuerArray[4] 901 | print "Issuer:\t\t" + str(certIssuer) 902 | certSubjectArray = str(certfile.get_subject()).replace('','').split('/') 903 | certSubject = certSubjectArray[1] + ', ' + certSubjectArray[2] + ', ' + certSubjectArray[3] + ', ' + certSubjectArray[4] 904 | print "Subject:\t" + str(certSubject) 905 | print "Version:\t" + str(certfile.get_version()) 906 | print "Serial number:\t" + str(certfile.get_serial_number()) 907 | validFromYear = str(certfile.get_notBefore())[:4] 908 | validFromMonth = str(certfile.get_notBefore())[4:6] 909 | validFromDay = str(certfile.get_notBefore())[6:8] 910 | validFromTime = str(certfile.get_notBefore())[8:10] + ':' + str(certfile.get_notBefore())[10:12] + ':' + str(certfile.get_notBefore())[12:14] 911 | print "Valid from:\t" + validFromYear + '-' + validFromMonth + '-' + validFromDay + ' ' + validFromTime 912 | validUntilYear = str(certfile.get_notAfter())[:4] 913 | validUntilMonth = str(certfile.get_notAfter())[4:6] 914 | validUntilDay = str(certfile.get_notAfter())[6:8] 915 | validUntilTime = str(certfile.get_notAfter())[8:10] + ':' + str(certfile.get_notAfter())[10:12] + ':' + str(certfile.get_notAfter())[12:14] 916 | print "Valid until:\t" + validUntilYear + '-' + validUntilMonth + '-' + validUntilDay + ' ' + validUntilTime 917 | if str(certfile.has_expired()) == '1': 918 | print "Expired:\tyes" 919 | else: 920 | print "Expired:\tno" 921 | 922 | 923 | def createSelfSignedCertificate(self, years): 924 | # check if a CA certificate already exists 925 | if os.path.exists(self.cacertfile): 926 | print "Error: the CA certificate already exists at %s" % self.cacertfile 927 | sys.exit() 928 | # check if a CA key already exists 929 | if os.path.exists(self.cakeyfile): 930 | # file already exists 931 | print "Warning: the CA keyfile already exists at %s, so we'll use it" % self.cakeyfile 932 | k = self.load_key(self.cakeyfile) 933 | else: 934 | # create a key pair 935 | k = crypto.PKey() 936 | k.generate_key(crypto.TYPE_RSA, int(default_bits)) 937 | valid_until = (date.today() + timedelta(int(years)*365)).isoformat() 938 | print "Generating a self-signed CA certificate." 939 | print "The certificate is valid until %s." % valid_until 940 | # create a self-signed cert 941 | cert = crypto.X509() 942 | # get some values from the openssl.cnf file 943 | cert.get_subject().C = countryName 944 | cert.get_subject().ST = stateOrProvinceName 945 | cert.get_subject().L = localityName 946 | cert.get_subject().O = organizationName 947 | cert.get_subject().OU = organizationalUnitName 948 | cert.get_subject().CN = gethostname() 949 | extensions = [] 950 | extensions.append(crypto.X509Extension('basicConstraints',0, 'CA:TRUE')) 951 | cert.add_extensions(extensions) 952 | serial = self.hex2dec( self.readSerial() ) 953 | cert.set_serial_number(serial) 954 | cert.gmtime_adj_notBefore(0) 955 | cert.gmtime_adj_notAfter(int(years)*365*24*60*60) 956 | cert.set_issuer(cert.get_subject()) 957 | cert.set_pubkey(k) 958 | cert.sign(k, default_md) 959 | # write key and cert file 960 | print "Writing CA private key to %s" % self.cakeyfile 961 | self.save_key (self.cakeyfile, k) 962 | print "Writing CA certificate to %s" % self.cacertfile 963 | self.save_cert (self.cacertfile, cert) 964 | 965 | 966 | # Generate keyfile and certificate 967 | def makeCert(self, fname, cname): 968 | # remove possible leftover files to prevent the zipfile from packing these 969 | for f in glob.glob(self.working + '/' + self.fprefix + fname + '.*'): 970 | if self.debug: print "DEBUG: removing old file %s" % f 971 | os.remove(f) 972 | pkey = self.createKeyPair(self.TYPE_RSA, int(default_bits)) 973 | req = self.createCertRequest(pkey, CN=cname, C=countryName, ST=stateOrProvinceName, O=organizationName, OU=organizationalUnitName) 974 | try: 975 | cacert = self.load_cert( self.cacertfile ) 976 | except: 977 | print "Error opening CA cert file" 978 | sys.exit() 979 | try: 980 | cakey = self.load_key(self.cakeyfile) 981 | except: 982 | print "Error opening CA key file" 983 | sys.exit() 984 | 985 | # check if the 'next serial number' in serialfile is the same as the serial number of the 986 | # last entry in the indexdb. If it is, increase the next serial by one (hex) and write a 987 | # new serialfile 988 | last = None 989 | for line in open(indexdb): 990 | last=line 991 | if last: 992 | last_serial = last.split("\t")[3].strip() 993 | else: 994 | last_serial = 0 995 | if self.debug: print "Last serial in indexdb: '%s'" % last_serial 996 | f=open(serialfile, 'r') 997 | serial = f.readline().strip() 998 | f.close() 999 | if self.debug: print "Next serial in serialfile: '%s'" % serial 1000 | if serial == last_serial: 1001 | print "Whoops! Last serial number in indexdb is the same as the next" 1002 | print "one in serialfile: %s. This is probably caused by an older version" % serial 1003 | print "of StoneVPN. We'll need to correct this (once) by increasing" 1004 | newSerialDec = self.hex2dec(serial) + 1 1005 | newSerial = self.dec2hex(newSerialDec) 1006 | print "the value for next serial number to %s" % newSerial 1007 | if len(newSerial) == 1: 1008 | newSerial = '0' + str(newSerial) 1009 | if self.debug: print "Now increasing %s by 1 to %s" % (serial,newSerial) 1010 | f=open(serialfile, 'w') 1011 | f.write(newSerial) 1012 | f.close() 1013 | 1014 | # read next serial number from serialfile 1015 | curSerial = self.readSerial() 1016 | 1017 | # format current time as UTC, for certificate 1018 | timeNow = datetime.utcnow() 1019 | # format current time as local, for indexdb 1020 | timeNowIdx = datetime.now() 1021 | 1022 | # We can't work with hex numbers. Convert them to dec first and increase its value by 1 1023 | newSerial = self.hex2dec(curSerial) + 1 1024 | newSerialDec = newSerial 1025 | # Now convert dec back to hex 1026 | newSerial = self.dec2hex(newSerial) 1027 | 1028 | # Check if a different expiration date for certificate 1029 | if self.expiredate: 1030 | # Check for valid arguments: (h)ours, (d)ays, (y)ears. 1031 | # For example: 2h or 6d or 2y. A combination is not (yet?) possible. 1032 | expList = list(self.expiredate) 1033 | try: 1034 | unit = list(self.expiredate)[-1] 1035 | if self.debug: print "DEBUG: time unit is %s" % unit 1036 | except: 1037 | print "Incorrect or missing time unit. Use h(ours), d(ays) or y(ears)." 1038 | sys.exit() 1039 | countRest = len(expList) - 1 1040 | exp_time = ''.join(expList[0:countRest]) 1041 | if self.debug: print "DEBUG: exp_time is %s" % exp_time 1042 | if unit not in ('h', 'H', 'd', 'D', 'y', 'Y'): 1043 | print "Invalid time unit provided. Use h(ours), d(ays) or y(ears)." 1044 | sys.exit() 1045 | elif unit in ('h', 'H'): 1046 | cert = self.createCertificate(req, (cacert, cakey), curSerial, (0, 60 * 60 * int(exp_time))) 1047 | expDate = timeNow + timedelta(hours=int(exp_time)) 1048 | expDateIdx = timeNowIdx + timedelta(hours=int(exp_time)) 1049 | print "Certificate is valid for %s hour(s)." % exp_time 1050 | elif unit in ('d', 'D'): 1051 | cert = self.createCertificate(req, (cacert, cakey), curSerial, (0, 24 * 60 * 60 * int(exp_time))) 1052 | expDate = timeNow + timedelta(days=int(exp_time)) 1053 | expDateIdx = timeNowIdx + timedelta(days=int(exp_time)) 1054 | print "Certificate is valid for %s day(s)." % exp_time 1055 | elif unit in ('y', 'Y'): 1056 | cert = self.createCertificate(req, (cacert, cakey), curSerial, (0, 24 * 60 * 60 * 365 * int(exp_time))) 1057 | expDate = timeNow + timedelta(days=int(exp_time) * 365) 1058 | expDateIdx = timeNowIdx + timedelta(days=int(exp_time) * 365) 1059 | print "Certificate is valid for %s year(s)." % exp_time 1060 | else: 1061 | cert = self.createCertificate(req, (cacert, cakey), curSerial, (0, 24 * 60 * 60 * int(defaultDays))) 1062 | expDate = timeNow + timedelta(days=int(defaultDays)) 1063 | expDateIdx = timeNowIdx + timedelta(days=int(defaultDays)) 1064 | print "Certificate is valid for %s day(s)." % defaultDays 1065 | self.save_key ( self.working + '/' + self.fprefix + fname + '.key', pkey ) 1066 | self.save_cert ( self.working + '/' + self.fprefix + fname + '.crt', cert ) 1067 | 1068 | # OpenSSL only accepts serials of 2 digits, so check for the length and prepend a 0 if necessary 1069 | if len(str(newSerial)) == 1: 1070 | serialIdx = '0' + str(newSerial) 1071 | else: 1072 | serialIdx = newSerial 1073 | # Write serial (hex) to serial file 1074 | self.writeSerial(serialIdx) 1075 | # copy CA certificate to working dir 1076 | shutil.copy(self.cacertfile, self.working) 1077 | # create the configuration files (default 'unix' unless specified with option -c) 1078 | self.makeConfs(self.confs, fname) 1079 | # write index to file 1080 | if self.debug: print "DEBUG: timeNow is %s" % timeNow 1081 | if self.debug: print "DEBUG: expDate is %s" % expDate 1082 | # OpenSSL only accepts serials of 2 digits, so check for the length and prepend a 0 if necessary 1083 | if len(str(curSerial)) == 1: 1084 | serialNumber = '0' + str(curSerial) 1085 | else: 1086 | serialNumber = curSerial 1087 | # convert cname: spaces to underscores for inclusion in indexdb 1088 | nospaces_cname = cname.replace(' ', '_') 1089 | # the expire date for the index file needs some conversion 1090 | indexDate = expDateIdx.strftime("%y%m%d%H%M%S") 1091 | if self.debug: print "DEBUG: indexDate is %s" % indexDate 1092 | # Format index line and write to OpenSSL index file 1093 | index = 'V\t' + str(indexDate) + 'Z\t\t' + str(serialNumber.strip()) + '\tunknown\t' + '/C=' + str(countryName) + '/ST=' + str(stateOrProvinceName) + '/O=' + str(organizationName) + '/OU=' + str(organizationalUnitName) + '/CN=' + str(nospaces_cname) + '/emailAddress=' + str(fname) + '@local\n' 1094 | self.writeIndex(index) 1095 | 1096 | # Make config files for OpenVPN 1097 | def makeConfs(self, sname, fname): 1098 | config = ConfigObj(self.stonevpnconf) 1099 | # Generate appropriate (according to specified OS) configuration for OpenVPN 1100 | if sname == 'unix' or sname == 'linux': 1101 | sectionname = 'unix conf' 1102 | print "Generating UNIX configuration file" 1103 | f=open(self.working + '/' + self.fprefix + fname + '.conf', 'w') 1104 | elif sname == 'windows': 1105 | sectionname = 'windows conf' 1106 | print "Generating Windows configuration file" 1107 | f=open(self.working + '/' + self.fprefix + fname + '.ovpn', 'w') 1108 | elif sname == 'mac': 1109 | sectionname = 'mac conf' 1110 | print "Generating Mac configuration file" 1111 | f=open(self.working + '/' + self.fprefix + fname + '.conf', 'w') 1112 | elif sname == 'android': 1113 | sectionname = 'android conf' 1114 | print "Generating Android configuration file" 1115 | f=open(self.working + '/' + self.fprefix + fname + '.ovpn', 'w') 1116 | elif sname == 'all': 1117 | print "Generating all configuration files" 1118 | else: 1119 | print "Incorrect OS type specified. Valid options are 'unix', 'windows', 'mac', 'android' or 'all'." 1120 | sys.exit() 1121 | if sname != 'all': 1122 | section=config[sectionname] 1123 | # Go over each entry (variable) and write it to the OpenVPN configuration file 1124 | for var in section: 1125 | # Fill in correct path to generated cert/key/cacert files 1126 | if var == 'ca': 1127 | cacertfilenopath = self.cacertfile.split('/')[int(len(self.cacertfile.split('/')) - 1)] 1128 | f.write(section[var].replace('cacertfile', cacertfilenopath) + '\n') 1129 | elif var == 'cert': 1130 | f.write(section[var].replace('clientcertfile', self.fprefix + fname + '.crt') + '\n') 1131 | elif var == 'key': 1132 | f.write(section[var].replace('clientkeyfile', self.fprefix + fname + '.key') + '\n') 1133 | elif var == 'ip': 1134 | if self.server_ip: 1135 | f.write("remote " + str(self.server_ip) + "\n") 1136 | else: 1137 | f.write(section[var] + '\n') 1138 | else: 1139 | f.write(section[var] + '\n') 1140 | if sname == 'android': 1141 | fp = open ( self.cacertfile, 'r' ) 1142 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1143 | fp.close () 1144 | fp = open ( self.working + '/' + self.fprefix + fname + '.crt', 'r' ) 1145 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1146 | fp.close () 1147 | fp = open ( self.working + '/' + self.fprefix + fname + '.key', 'r' ) 1148 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1149 | fp.close () 1150 | f.close() 1151 | else: 1152 | os_versions = ["windows", "linux", "mac", "android"] 1153 | for os_type in os_versions: 1154 | # soort extensie ipv deze regel << 1155 | if os_type == 'linux': 1156 | sectionname = 'unix conf' 1157 | print "Generating Linux configuration file" 1158 | f=open(self.working + '/' + self.fprefix + fname + '.linux.conf', 'w') 1159 | elif os_type == 'windows': 1160 | sectionname = 'windows conf' 1161 | print "Generating Windows configuration file" 1162 | f=open(self.working + '/' + self.fprefix + fname + '.windows.ovpn', 'w') 1163 | elif os_type == 'mac': 1164 | sectionname = 'mac conf' 1165 | print "Generating Mac configuration file" 1166 | f=open(self.working + '/' + self.fprefix + fname + '.mac.conf', 'w') 1167 | elif os_type == 'android': 1168 | sectionname = 'android conf' 1169 | print "Generating Android configuration file" 1170 | f=open(self.working + '/' + self.fprefix + fname + '.android.ovpn', 'w') 1171 | section=config[sectionname] 1172 | for var in section: 1173 | if var == 'ca': 1174 | cacertfilenopath = self.cacertfile.split('/')[int(len(self.cacertfile.split('/')) - 1)] 1175 | f.write(section[var].replace('cacertfile', cacertfilenopath) + '\n') 1176 | elif var == 'cert': 1177 | f.write(section[var].replace('clientcertfile', self.fprefix + fname + '.crt') + '\n') 1178 | elif var == 'key': 1179 | f.write(section[var].replace('clientkeyfile', self.fprefix + fname + '.key') + '\n') 1180 | else: 1181 | f.write(section[var] + '\n') 1182 | if os_type == 'android': 1183 | fp = open ( self.cacertfile, 'r' ) 1184 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1185 | fp.close () 1186 | fp = open ( self.working + '/' + self.fprefix + fname + '.crt', 'r' ) 1187 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1188 | fp.close () 1189 | fp = open ( self.working + '/' + self.fprefix + fname + '.key', 'r' ) 1190 | f.write('\n' + "" + '\n' + fp.read() + "" + '\n') 1191 | fp.close () 1192 | f.close() 1193 | 1194 | 1195 | # Revoke certificate 1196 | def revokeCert(self, serial): 1197 | if not os.path.exists(self.crlfile): 1198 | print "Error: CRL file not found at: " + self.crlfile + " or insufficient rights." 1199 | sys.exit() 1200 | try: 1201 | crl = crypto.CRL() 1202 | except: 1203 | print "\nError: CRL support is not available in your version of" 1204 | print "pyOpenSSL. Please check the README file that came with" 1205 | print "StoneVPN to see what you can do about this. For now, " 1206 | print "you will have to revoke certificates manually.\n" 1207 | sys.exit() 1208 | # we can't replace stuff in the original index file, so we have to create 1209 | # a new one and in the end rename the original one and move the temp file 1210 | # to the final location (usually /etc/ssl/index.txt) 1211 | t=open(self.working + '/index.tmp', 'w') 1212 | # read SSL dbase from the index file 1213 | # this file has 5 columns: Status, Expiry date, Revocation date, Serial nr, file?, Distinguished Name (DN) 1214 | print "Reading SSL database: " + indexdb 1215 | input = open(indexdb, 'r') 1216 | f=open(self.working + '/revoked.crl', 'w') 1217 | crlTime = str(strftime("%y%m%d%H%M%S")) + 'Z' 1218 | for line in input: 1219 | # first check if the line contains a revoked cert: 1220 | if line.split()[0] == 'R': 1221 | # then check if the revoked cert has the same serial nr as the one we're trying to revoke 1222 | # if so, exit immediately since we can't revoke twice (duh) 1223 | if line.split()[3] == serial.upper(): 1224 | print "Certificate with serial %s already revoked!" % serial.upper() 1225 | os.remove(self.working + '/index.tmp') 1226 | os.remove(self.working + '/revoked.crl') 1227 | sys.exit() 1228 | else: 1229 | revSerial = str(line.split()[3]) 1230 | revDate = str(line.split()[2]) 1231 | revoked = crypto.Revoked() 1232 | revoked.set_rev_date('20' + str(revDate)) 1233 | revoked.set_serial(revSerial) 1234 | #no reason needed? 1235 | #revoked.set_reason('revoked') 1236 | crl.add_revoked(revoked) 1237 | # /new way 1238 | print "Re-adding existing revoked certificate to CRL with date " + revDate + " and serial " + revSerial 1239 | t.write(line) 1240 | else: 1241 | # the line contains a valid certificate. Check if the serial is the same as the 1242 | # one we're trying to revoke 1243 | if line.split()[2] == serial.upper(): 1244 | # we have a match! do not write this line again to the new index file 1245 | # instead, change it to the revoked-format 1246 | if self.debug: print "DEBUG: found match for %s in index file" % serial.upper() 1247 | newDN = '/'.join(line.split('/')[1:]) 1248 | revokedLine = 'R\t' + str(line.split()[1]) + '\t' + crlTime + '\t' + serial.upper() + '\tunknown\t' + str(newDN) 1249 | t.write(revokedLine) 1250 | else: 1251 | # this is not the match we're looking for, so just write the line again 1252 | # to the index file 1253 | t.write(line) 1254 | # crlTime = str(strftime("%y%m%d%H%M%S")) + 'Z' 1255 | print "Adding new revoked certificate to CRL with date " + crlTime + " and serial " + serial.upper() 1256 | t.close() 1257 | revoked = crypto.Revoked() 1258 | now = datetime.utcnow().strftime("%Y%m%d%H%M%SZ") 1259 | revoked.set_rev_date(now) 1260 | revoked.set_serial(serial) 1261 | #no reason needed? 1262 | #revoked.set_reason('sUpErSeDEd') 1263 | crl.add_revoked(revoked) 1264 | cacert = self.load_cert(self.cacertfile) 1265 | cakey = self.load_key(self.cakeyfile) 1266 | newCRL = crl.export(cacert, cakey, days=20) 1267 | f.write(newCRL) 1268 | f.close() 1269 | shutil.move(indexdb,indexdb + '.old') 1270 | shutil.move(self.working + '/index.tmp',indexdb) 1271 | shutil.move(self.crlfile,self.crlfile + '.old') 1272 | shutil.move(self.working + '/revoked.crl',self.crlfile) 1273 | print "New CRL written to: %s. Backup created as: %s." % (self.crlfile,self.crlfile + '.old') 1274 | print "New index written to: %s. Backup created as: %s." % (indexdb,indexdb + '.old') 1275 | 1276 | def displayCRL(self): 1277 | if not os.path.exists(self.crlfile): 1278 | print "Error: CRL file not found at %s" % self.crlfile 1279 | print "You can create one with: stonevpn --newcrl" 1280 | sys.exit() 1281 | text = open(self.crlfile, 'r').read() 1282 | print "Parsing CRL file %s" % self.crlfile 1283 | try: 1284 | crl = crypto.load_crl(crypto.FILETYPE_PEM, text) 1285 | revs = crl.get_revoked() 1286 | except: 1287 | print "\nError: CRL support is not available in your version of" 1288 | print "pyOpenSSL. Please check the README file that came with" 1289 | print "StoneVPN to see what you can do about this. For now, " 1290 | print "you will have to display the CRL file manually using:\n" 1291 | print "$ openssl crl -in %s -noout -text\n" % self.crlfile 1292 | sys.exit() 1293 | if not revs is None: 1294 | print "Total certificates revoked: %s\n" % len(revs) 1295 | print "Serial\tRevoked at date" 1296 | print "======\t========================" 1297 | for revoked in revs: 1298 | revSerial = revoked.get_serial() 1299 | revDate = revoked.get_rev_date()[0:-1] 1300 | revoDate = time.strptime(revDate, "%Y%m%d%H%M%S") 1301 | print str(revSerial) + "\t" + time.strftime("%c", revoDate) 1302 | else: 1303 | print "No revoked certificates found." 1304 | 1305 | 1306 | def listRevokedCerts(self): 1307 | # read SSL dbase (usually index.txt) 1308 | # this file has 5 columns: Status, Expiry date, Revocation date, Serial nr, unknown, Distinguished Name (DN) 1309 | print "Reading SSL database: " + indexdb 1310 | input = open(indexdb, 'r') 1311 | revCerts = [] 1312 | print "Finding revoked certificates..." 1313 | for line in input: 1314 | if line.split()[0] == 'R': 1315 | revCerts.append(line) 1316 | count = 0 1317 | while count < len(revCerts): 1318 | #print "Revoked certificate:\t" + str(revCerts[count].split()[5].split('/CN=')[1]) 1319 | print "Issued to:\t\t%s" % str(revCerts[count]).split('CN=')[1].split('/')[0] 1320 | print "Status:\t\t\tRevoked" 1321 | expDate = str(revCerts[count].split()[1]) 1322 | print "Expiry date:\t\t20%s-%s-%s %s:%s:%s" % (expDate[:2],expDate[2:4],expDate[4:6],expDate[6:8],expDate[8:10],expDate[10:12]) 1323 | revDate = str(revCerts[count].split()[2]) 1324 | print "Revocation date:\t20%s-%s-%s %s:%s:%s" % (revDate[:2],revDate[2:4],revDate[4:6],revDate[6:8],revDate[8:10],revDate[10:12]) 1325 | print "Serial:\t\t\t%s" % str(revCerts[count].split()[3]) 1326 | lineDN = line.split('unknown')[1].strip() 1327 | newDN = ''.join(lineDN).replace('/',',') 1328 | print "DN:\t\t\t%s" % newDN 1329 | print "\n" 1330 | count = count + 1 1331 | 1332 | 1333 | def indent(self, rows, hasHeader=False, headerChar='-', delim=' | ', justify='left', 1334 | separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x): 1335 | # closure for breaking logical rows to physical, using wrapfunc 1336 | def rowWrapper(row): 1337 | newRows = [wrapfunc(item).split('\n') for item in row] 1338 | return [[substr or '' for substr in item] for item in map(None,*newRows)] 1339 | # break each logical row into one or more physical ones 1340 | logicalRows = [rowWrapper(row) for row in rows] 1341 | # columns of physical rows 1342 | columns = map(None,*reduce(operator.add,logicalRows)) 1343 | # get the maximum of each column by the string length of its items 1344 | maxWidths = [max([len(str(item)) for item in column]) for column in columns] 1345 | rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \ 1346 | len(delim)*(len(maxWidths)-1)) 1347 | # select the appropriate justify method 1348 | justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()] 1349 | output=cStringIO.StringIO() 1350 | if separateRows: print >> output, rowSeparator 1351 | for physicalRows in logicalRows: 1352 | for row in physicalRows: 1353 | print >> output, \ 1354 | prefix \ 1355 | + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \ 1356 | + postfix 1357 | if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False 1358 | return output.getvalue() 1359 | 1360 | def listAllCerts(self): 1361 | # list all certificates in indexdb, output as pretty table 1362 | input = open(indexdb, 'r') 1363 | data = '' 1364 | for line in input: 1365 | if line.split()[0] == 'R': 1366 | issuee = line.split('/')[-2:][0].replace('CN=','').replace('_',' ').replace(',','_') 1367 | data = str(data) + str(line.split()[3]) + ',' + str(issuee) 1368 | data = str(data) + ',' + "Revoked" 1369 | revDate = str(line.split()[2]).replace('Z','') 1370 | data = str(data) + ',' + '20' + revDate[:2] + '-' + revDate[2:4] + '-' + revDate[4:6] + ' ' + revDate[6:8] + ':' + revDate[8:10] + ':' + revDate[10:12] + '\n' 1371 | else: 1372 | issuee = line.split('/')[-2:-1][0].split('\t')[0].replace('CN=','').replace('_',' ').replace(',','_') 1373 | data = str(data) + str(line.split()[2]) + ',' + str(issuee) 1374 | expDate = str(line.split()[1]).replace('Z','') 1375 | ed_long = "20" + str(expDate) 1376 | expiredate = int(time.mktime(time.strptime(str(datetime(int(ed_long[:4]),int(ed_long[4:6]),int(ed_long[6:8]),int(ed_long[8:10]),int(ed_long[10:12]),int(ed_long[12:14]))),"%Y-%m-%d %H:%M:%S"))) 1377 | timenow = int(time.mktime(time.localtime())) 1378 | if int(timenow) < int(expiredate): 1379 | data = str(data) + ',Valid' 1380 | else: 1381 | data = str(data) + ',Expired' 1382 | data = str(data) + ',' + '20' + expDate[:2] + '-' + expDate[2:4] + '-' + expDate[4:6] + ' ' + expDate[6:8] + ':' + expDate[8:10] + ':' + expDate[10:12] + '\n' 1383 | 1384 | rows = [] 1385 | rows.append(('Serial','Name','Status','Expiry date')) 1386 | for row in data.splitlines(): 1387 | rows.append(row.strip().split(',')) 1388 | width = 60 1389 | print self.indent(rows,hasHeader=True) 1390 | 1391 | 1392 | 1393 | def listAllCertsCSV(self): 1394 | # same routine as listAllCerts() except print as 1395 | # comma seperated values and without the DN. 1396 | input = open(indexdb, 'r') 1397 | for line in input: 1398 | if line.split()[0] == 'R': 1399 | issuee = line.split('/')[-2:][0].replace('CN=','').replace('_',' ').replace(',','_') 1400 | sys.stdout.write(str(issuee) + ",") 1401 | sys.stdout.write("Revoked,") 1402 | revDate = str(line.split()[2]).replace('Z','') 1403 | sys.stdout.write("20%s-%s-%s %s:%s:%s," % (revDate[:2],revDate[2:4],revDate[4:6],revDate[6:8],revDate[8:10],revDate[10:12])) 1404 | sys.stdout.write(str(line.split()[3]) + ",") 1405 | else: 1406 | issuee = line.split('/')[-2:-1][0].split('\t')[0].replace('CN=','').replace('_',' ').replace(',','_') 1407 | sys.stdout.write(str(issuee) + ",") 1408 | expDate = str(line.split()[1]).replace('Z','') 1409 | ed_long = "20" + str(expDate) 1410 | expiredate = int(time.mktime(time.strptime(str(datetime(int(ed_long[:4]),int(ed_long[4:6]),int(ed_long[6:8]),int(ed_long[8:10]),int(ed_long[10:12]),int(ed_long[12:14]))),"%Y-%m-%d %H:%M:%S"))) 1411 | timenow = int(time.mktime(time.localtime())) 1412 | if int(timenow) < int(expiredate): 1413 | sys.stdout.write("Valid,") 1414 | else: 1415 | sys.stdout.write("Expired,") 1416 | sys.stdout.write("20%s-%s-%s %s:%s:%s," % (expDate[:2],expDate[2:4],expDate[4:6],expDate[6:8],expDate[8:10],expDate[10:12])) 1417 | sys.stdout.write(str(line.split()[2]) + ",") 1418 | 1419 | def send_mail(self, send_from, send_to, subject, text, attachment=[]): 1420 | print "Generating e-mail" 1421 | msg = MIMEMultipart() 1422 | msg['From'] = send_from 1423 | msg['To'] = send_to 1424 | msg['CC'] = self.mail_cc 1425 | msg['Date'] = formatdate(localtime=True) 1426 | msg['Subject'] = subject 1427 | text = text.replace('EMAILRECIPIENT', self.cname) 1428 | # Append a helpful text when a password was given, but only when specified on the commandline 1429 | if self.mailpass: 1430 | if self.passphrase is None and self.randpass is None: 1431 | print "Error: you need to specify either a passphrase or generate a random one." 1432 | sys.exit() 1433 | if keyPass: 1434 | if self.debug: print "DEBUG: including password help text in email body" 1435 | text = text.replace('PASSPHRASETXT', self.mail_passtxt) 1436 | # And replace the password placeholder with the actual passphrase 1437 | text = text.replace('OPENSSLPASS', keyPass) 1438 | else: 1439 | text = text.replace('PASSPHRASETXT', '') 1440 | msg.attach( MIMEText(text, 'html') ) 1441 | # Attachment(s) 1442 | if type(attachment) == 'string': 1443 | part = MIMEBase('application', "octet-stream") 1444 | part.set_payload( open(attachment,"rb").read() ) 1445 | Encoders.encode_base64(part) 1446 | part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(attachment)) 1447 | msg.attach(part) 1448 | else: 1449 | for f in attachment: 1450 | print "Attaching file %s" % f 1451 | part = MIMEBase('application', "octet-stream") 1452 | part.set_payload( open(f,"rb").read() ) 1453 | Encoders.encode_base64(part) 1454 | part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f)) 1455 | msg.attach(part) 1456 | # Now to send the entire message 1457 | print 'Sending e-mail with attachment(s) to %s' % self.emailaddress 1458 | smtp = smtplib.SMTP(self.mail_server) 1459 | smtp.sendmail(send_from, send_to, msg.as_string()) 1460 | smtp.close() 1461 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | BUGS: 2 | - print help text when no options are given 3 | - printing info about 1 specific cert (option '--printcert') shows non-local timezone 4 | - don't update indexdb if cert can't be generated (due to missing X509Ext method in pyOpenSSL for example) 5 | - also ask for overwrite clientfile if looking for freeip 6 | - when calculating freeip: don't parse clientfile itself if it exists 7 | 8 | NICETOHAVES: 9 | - option to purge expired certificates from indexdb 10 | - check for all required fields in stonevpn.conf 11 | - parse openssl.cnf for ENV variables 12 | - option to (PGP/GPG) encrypt e-mail 13 | - option to use a different configuration file (defaults to /etc/stonevpn.conf) 14 | - option to create encrypted zipfile (using 7zip?) 15 | - a web interface would be cool .. in Django! python kicks butt 16 | - log all actions/output 17 | - port to Windows 18 | -------------------------------------------------------------------------------- /bin/stonevpn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | StoneVPN wrapper script. 5 | 6 | Copyright 2009-2012 L.S. Keijser 7 | 8 | This software may be freely redistributed under the terms of the GNU 9 | general public license. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program; if not, write to the Free Software 13 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 14 | """ 15 | 16 | import sys 17 | import StoneVPN.app as app 18 | sys.exit(app.main() or 0) 19 | 20 | -------------------------------------------------------------------------------- /conf/stonevpn.conf: -------------------------------------------------------------------------------- 1 | [stonevpn conf] 2 | # CA certificate file 3 | cacertfile = '/etc/openvpn/server.crt' 4 | # CA private key file (make sure running user has read rights!) 5 | cakeyfile = '/etc/openvpn/server.key' 6 | # This is needed to search for free IP-addresses 7 | openvpnconf = '/etc/openvpn/vpnserver.conf' 8 | # Search for free IP-adresses by parsing the files in this dir 9 | ccddir = '/etc/openvpn/ccd' 10 | # Temporary working dir (will be created if it doesn't exist) 11 | working = '/var/stonevpn' 12 | # OpenSSL configuration file 13 | opensslconf = '/etc/pki/tls/openssl.cnf' 14 | # push router ip (Only used with '--free-ip' parameter) 15 | pushrouter = '10.11.12.13' 16 | # Certificate Revocation List (CRL) file 17 | crlfile = '/etc/openvpn/stonevpn.crl' 18 | # if using password, which cipher method to use (openssl --help) 19 | cipher = 'des3' 20 | # prefix all files with: 21 | prefix = 'mycorp-' 22 | # For emailing generated files, specify SMTP server 23 | mail_server = '127.0.0.1' 24 | # Send CC to (leave blank (mail_cc = '') for none) 25 | mail_cc = 'me@example.com,helpdesk@example.com' 26 | # Email 'From' address 27 | mail_from = 'stonevpn@example.com' 28 | # Mail body (HTML formatted), don't change keyword EMAILRECIPIENT 29 | mail_msg = "Hi EMAILRECIPIENT,

Attached with this e-mail are the generated configuration file and certificates for use with your VPN connection. PASSPHRASETXT If you have any questions, please contact support

Kind regards,

The Support department." 30 | # Text to include when specifying a passhprase. This will be inserted 31 | # in 'mail_msg' on placeholder PASSPRASETXT. If you don't use it, PASSPHRASETXT 32 | # will be blanked out. Also, don't change keyword OPENSSLPASS as this will be 33 | # replaced by the actual passphrase. 34 | mail_passtxt = '
For security purposes, your key is encrypted with this password:

OPENSSLPASS

You will be prompted for this password when establishing a connection.' 35 | 36 | [windows conf] 37 | # add options to be added to the configuration file here 38 | # it doesn't really matter what the variable's name is :) 39 | dev = 'dev tap' 40 | ip = 'remote 12.34.56.78' 41 | # uncomment the next 3 lines to add redundant routers: 42 | # remote-random 43 | # resolv-retry 60 44 | # ip2 = 'remote 23.45.67.89' 45 | port = 'port 1194' 46 | mssfix = 'mssfix 1300' 47 | client = 'client' 48 | tls = 'tls-client' 49 | # actually for the next 3 vars, the name _does_ matter since 50 | # we'll do some string replace stuff (to get the right filename) 51 | ca = 'ca "c:\\Program Files\\OpenVPN\\config\\cacertfile"' 52 | cert = 'cert "c:\\Program Files\\OpenVPN\\config\\clientcertfile"' 53 | key = 'key "c:\\Program Files\\OpenVPN\\config\\clientkeyfile"' 54 | lzo = 'comp-lzo' 55 | ping = 'ping 15' 56 | pingrestart = 'ping-restart 45' 57 | pingtimer = 'ping-timer-rem' 58 | persisttun = 'persist-tun' 59 | persistkey = 'persist-key' 60 | verb = 'verb 3' 61 | prot = 'proto tcp' 62 | float = 'float' 63 | 64 | [unix conf] 65 | # add options to be added to the configuration file here 66 | daemon = 'daemon' 67 | dev = 'dev stonevpn' 68 | devtype = 'dev-type tap' 69 | ip = 'remote 12.34.56.78' 70 | # uncomment the next 3 lines to add redundant routers: 71 | # remote-random 72 | # resolv-retry 60 73 | # ip2 = 'remote 23.45.67.89' 74 | port = 'port 1194' 75 | mssfix = 'mssfix 1300' 76 | client = 'client' 77 | tlc = 'tls-client' 78 | # don't touch the next 3 var names: 79 | ca = 'ca /etc/openvpn/cacertfile' 80 | cert = 'cert /etc/openvpn/clientcertfile' 81 | key = 'key /etc/openvpn/clientkeyfile' 82 | lzo = 'comp-lzo' 83 | ping = 'ping 15' 84 | pingrestart = 'ping-restart 45' 85 | pingtimer = 'ping-timer-rem' 86 | persisttun = 'persist-tun' 87 | persistkey = 'persist-key' 88 | verb = 'verb 3' 89 | prot = 'proto tcp' 90 | 91 | [mac conf] 92 | # add options to be added to the configuration file here 93 | daemon = 'daemon' 94 | dev = 'dev tap' 95 | ip = 'remote 12.34.56.78' 96 | # uncomment the next 3 lines to add redundant routers: 97 | # remote-random 98 | # resolv-retry 60 99 | # ip2 = 'remote 23.45.67.89' 100 | port = 'port 1194' 101 | mssfix = 'mssfix 1300' 102 | client = 'client' 103 | tlc = 'tls-client' 104 | # don't touch the next 3 var names: 105 | ca = 'ca /Library/openvpn/cacertfile' 106 | cert = 'cert /Library/openvpn/clientcertfile' 107 | key = 'key /Library/openvpn/clientkeyfile' 108 | lzo = 'comp-lzo' 109 | ping = 'ping 15' 110 | pingrestart = 'ping-restart 45' 111 | pingtimer = 'ping-timer-rem' 112 | persisttun = 'persist-tun' 113 | persistkey = 'persist-key' 114 | verb = 'verb 3' 115 | prot = 'proto tcp' 116 | 117 | [android conf] 118 | # add options to be added to the configuration file here 119 | daemon = 'daemon' 120 | dev = 'dev tun' 121 | ip = 'remote 12.34.56.78' 122 | # uncomment the next 3 lines to add redundant routers: 123 | # remote-random 124 | # resolv-retry 60 125 | # ip2 = 'remote 23.45.67.89' 126 | port = 'port 1194' 127 | #mssfix = 'mssfix 1300' 128 | client = 'client' 129 | tlc = 'tls-client' 130 | lzo = 'comp-lzo' 131 | ping = 'ping 15' 132 | pingrestart = 'ping-restart 45' 133 | pingtimer = 'ping-timer-rem' 134 | persisttun = 'persist-tun' 135 | persistkey = 'persist-key' 136 | verb = 'verb 3' 137 | prot = 'proto tcp' 138 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | stonevpn (0.4.5-1) unstable; urgency=low 2 | 3 | * New version from upstream 4 | 5 | -- Léon Keijser Tue, 02 Feb 2010 21:44:40 +0100 6 | 7 | stonevpn (0.4.4-1) unstable; urgency=low 8 | 9 | * New version from upstream 10 | 11 | -- Léon Keijser Mon, 09 Nov 2009 07:22:48 +0100 12 | 13 | stonevpn (0.4.3-1) unstable; urgency=low 14 | 15 | * Initial release (Closes: #nnnn) 16 | 17 | -- Léon Keijser Fri, 30 Oct 2009 10:28:13 +0100 18 | 19 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: stonevpn 2 | Section: net 3 | Priority: optional 4 | Maintainer: Léon Keijser 5 | Build-Depends: debhelper (>= 5) 6 | Standards-Version: 3.7.2 7 | 8 | Package: stonevpn 9 | Architecture: any 10 | Depends: python (>=2.4), python-pyopenssl (>=0.9), python-ipy (>=0.55), python-configobj (>=4.4.0) 11 | Description: Easy OpenVPN certificate and configuration management 12 | StoneVPN allows you to manage OpenVPN certificates and create 13 | configurations for Windows and Linux machines based on a 14 | template. It can package everything into a zipfile and mail 15 | it to a user. 16 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This package was debianized by Léon Keijser on 2 | Fri, 30 Oct 2009 10:28:13 +0100. 3 | 4 | Upstream Author: Léon Keijser, 5 | 6 | License: 7 | 8 | This package is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This package is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this package; if not, write to the Free Software 20 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | 22 | On Debian systems, the complete text of the GNU General 23 | Public License can be found in `/usr/share/common-licenses/GPL'. 24 | 25 | The Debian packaging is (C) 2009, Léon Keijser and 26 | is licensed under the GPL, see above. 27 | 28 | 29 | # Please also look if there are files or directories which have a 30 | # different copyright/license attached and list them here. 31 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | 13 | 14 | 15 | configure: configure-stamp 16 | configure-stamp: 17 | dh_testdir 18 | # Add here commands to configure the package. 19 | 20 | touch configure-stamp 21 | 22 | 23 | build: build-stamp 24 | 25 | build-stamp: configure-stamp 26 | dh_testdir 27 | touch build-stamp 28 | 29 | clean: 30 | dh_testdir 31 | dh_testroot 32 | rm -f build-stamp configure-stamp 33 | dh_clean 34 | 35 | install: build 36 | dh_testdir 37 | dh_testroot 38 | dh_clean -k 39 | dh_installdirs 40 | 41 | python setup.py install 42 | 43 | 44 | 45 | # Build architecture-independent files here. 46 | binary-indep: build install 47 | # We have nothing to do by default. 48 | 49 | # Build architecture-dependent files here. 50 | binary-arch: build install 51 | dh_testdir 52 | dh_testroot 53 | dh_installchangelogs 54 | dh_installdocs 55 | dh_installexamples 56 | # dh_install 57 | # dh_installmenu 58 | # dh_installdebconf 59 | # dh_installlogrotate 60 | # dh_installemacsen 61 | # dh_installpam 62 | # dh_installmime 63 | # dh_python 64 | # dh_installinit 65 | # dh_installcron 66 | # dh_installinfo 67 | dh_installman 68 | dh_link 69 | dh_strip 70 | dh_compress 71 | dh_fixperms 72 | # dh_perl 73 | # dh_makeshlibs 74 | dh_installdeb 75 | dh_shlibdeps 76 | dh_gencontrol 77 | dh_md5sums 78 | dh_builddeb 79 | 80 | binary: binary-indep binary-arch 81 | .PHONY: build clean binary-indep binary-arch binary install configure 82 | -------------------------------------------------------------------------------- /man/stonevpn.1: -------------------------------------------------------------------------------- 1 | .TH STONEVPN 1 "May 2010" "" "StoneVPN User Manual" 2 | .SH NAME 3 | stonevpn \- Easy OpenVPN certificate and configuration management 4 | 5 | .SH SYNOPSIS 6 | .B stonevpn -f 7 | .I filename 8 | .B -n 9 | .I commonname 10 | .B [ OPTIONS ] 11 | 12 | .SH DESCRIPTION 13 | .B StoneVPN 14 | allows you to manage OpenVPN certificates and create 15 | configurations for Windows and Linux machines based on a 16 | template. It can package everything into a zipfile and mail 17 | it to a user. 18 | 19 | .SH OPTIONS 20 | .TP 21 | .BI --version 22 | Show program's version number and exit 23 | .TP 24 | .BI -h "\fR,\fB --help 25 | Show the help message and exit 26 | .TP 27 | .BI -D "\fR,\fB --debug 28 | Enable debugging information. You probably don't want to use this option as it prints quite useless information for normal usage. 29 | .TP 30 | .BI -n " CNAME" "\fR,\fP \-\^\-name=" CNAME 31 | Common Name, use quotes eg.: "John Cleese" 32 | .TP 33 | .BI -f " FNAME" "\fR,\fP \-\^\-file=" FNAME 34 | Write to file FNAME (no extension!) 35 | .TP 36 | .BI -o " CONFS" "\fR,\fP \-\^\-config=" CONFS 37 | Create config files for \fB[ \fIwindows\fR | \fIunix\fB | \fImac\fB | \fIall\fB ] \fR 38 | 39 | When supplying \fIall\fR StoneVPN will generate configuration files for all three Operating Systems. 40 | .TP 41 | .BI -e " FPREFIX" "\fR,\fP \-\^\-prefix=" FPREFIX 42 | Prefix (almost all) generated files. For example, if you set FPREFIX to 'mycorp', generated files will look like 'mycorp-user.crt/zip/key' 43 | .TP 44 | .BI -z \fR, \fB\-\^\-zip 45 | Package all generated files into a ZIP file. 46 | .TP 47 | .BI -m " EMAILADDRESS" "\fR,\fP \-\^\-mail=" EMAILADDRESS 48 | Send all generated files by e-mail to \fIEMAILADDRESS\fR. You might want to encrypt the user's key with a password when using this method. 49 | .TP 50 | .BI -i \fR, \fB\-\^\-free-ip 51 | Locate and assign free ip by parsing the OpenVPN server configuration file (more specifically the 'ifconfig-pool' line), and client configuration files within the ccd directory. 52 | .TP 53 | .BI -p \fR, \fB\-\^\-passphrase 54 | Prompt for a passphrase when generating the user's private key. Leave empty to provide one on the commandline. For example: 55 | 56 | stonevpn -f user -n "User Name" -p mysecret 57 | .TP 58 | .BI -M \fR, \fB\-\^\-mailpass 59 | Include passphrase in e-mail body (only useful with the '-m' option). You might want to change the mail_passtxt variable in stonevpn.conf as well. 60 | .TP 61 | .BI -R " RANDPASS" "\fR,\fP \-\^\-randpass=" RANDPASS 62 | Generate a random password of RANDPASS characters. For example, to generate an 8 character passphrase: 63 | 64 | stonevpn -f user -n "User Name" -R 8 65 | .TP 66 | .BI -E \fR, \fB\-\^\-extrafile 67 | Include extra files when generating a certificate. When also specifying the \fB\-\^\-zip\fR option, these will be packed in the zip file. Else, they will remain in a subdirectory of the working directory, based on the given FNAME. Use the full path to the filename to be included. 68 | You can use this option multiple times: 69 | 70 | stonevpn -f user -n "User Name" -E /path/to/file1 -E /path/to/file2 71 | .TP 72 | .BI -S \fR, \fB\-\^\-serverip 73 | Use this IP address for the server when generating the configuration file, overriding the one specified in stonevpn.conf 74 | .TP 75 | .BI -r " SERIAL" "\fR,\fP \-\^\-revoke=" SERIAL 76 | Revoke certificate with serial SERIAL 77 | .TP 78 | .BI -u " ROUTE" "\fR,\fP \-\^\-route=" ROUTE 79 | Push extra route(s) to client by means of a client configuration file on the server. For example: 80 | 81 | stonevpn -f user -n "User Name" -u 192.168.1.0/24 82 | 83 | You can specify multiple routes with another '-u '. This will write the route(s) to /etc/openvpn/cdd/Test_User 84 | .TP 85 | .BI -l \fR, \fB\-\^\-listrevoked 86 | List revoked certificates 87 | .TP 88 | .BI --crl 89 | Display CRL file contents 90 | .TP 91 | .BI -a \fR, \fB\-\^\-listall 92 | List all certificates 93 | .TP 94 | .BI -s \fR, \fB\-\^\-showserial 95 | Display current SSL serial number 96 | .TP 97 | .BI -c " PRINTCERT" "\fR,\fP \-\^\-printcert=" PRINTCERT 98 | Prints information about a certficiate file 99 | .TP 100 | .BI -d \fR, \fB\-\^\-printindex 101 | Prints index file 102 | .TP 103 | .BI -x " EXPIREDATE" "\fR,\fP \-\^\-expire=" EXPIREDATE 104 | Certificate expires in EXPIREDATE hours/days/years instead of the default specified in the openssl.cnf. For example: 105 | 106 | stonevpn -f user -n "User Name" -x 3h # valid for 3 hours 107 | stonevpn -f user -n "User Name" -x 2d # same, but 2 days 108 | stonevpn -f user -n "User Name" -x 1y # and for one year 109 | .TP 110 | .BI -N \fR, \fB\-\^\-newcrl 111 | Create an empty CRL file (or overwrite an existing one) 112 | .TP 113 | .BI -t \fR, \fB\-\^\-test 114 | Danger, Will Robinson, Danger! test parameter - can do 115 | anything! Review source before executing! 116 | 117 | .SH FILES 118 | .I /etc/stonevpn.conf 119 | .RS 120 | Configuration file. See 121 | .BR stonevpn (5) 122 | for further details. 123 | 124 | .SH EXAMPLES 125 | .TP 126 | Create a certificate and (Unix) configuration file for John Cleese and pack everything into johncleese.zip: 127 | 128 | stonevpn -f johncleese -n "John Cleese" -z 129 | 130 | .TP 131 | The same, but now encrypt the user's private key with a password and email the zipfile to them: 132 | 133 | stonevpn -f johncleese -n "John Cleese" -z -p -m user@domain.tld 134 | 135 | .SH BUGS 136 | Please report bugs on http://github.com/lkeijser/stonevpn/issues or mail the author. 137 | 138 | .SH AUTHOR 139 | Léon Keijser 140 | 141 | .SH "SEE ALSO" 142 | .RI stonevpn (5) 143 | -------------------------------------------------------------------------------- /man/stonevpn.conf.5: -------------------------------------------------------------------------------- 1 | .TH STONEVPN.CONF 1 "November 2009" "" "StoneVPN Configuration File" 2 | .SH NAME 3 | stonevpn.conf \- configuration file for OpenVPN 4 | 5 | .SH SYNOPSIS 6 | .B stonevpn.conf 7 | 8 | .SH DESCRIPTION 9 | .I stonevpn.conf 10 | is the configuration file for \fBStoneVPN\fR. It follows the rules for 11 | .B python-configobj 12 | 13 | .SH SECTIONS 14 | .TP 15 | The configuration file has three sections: 16 | .TP 17 | .B "[stonevpn conf]" 18 | generic configuration for the program, such as the paths to OpenSSL-related files 19 | .TP 20 | .B "[windows conf]" 21 | configuration template to generate OpenVPN configuration files for Microsoft Windows (TM) 22 | .TP 23 | .B "[unix conf]" 24 | configuration template to generate OpenVPN configuration files for *nix systems 25 | 26 | .SH FILES 27 | .TP 28 | /etc/stonevpn.conf 29 | .RS 30 | Configuration file for 31 | .RI stonevpn (1) 32 | 33 | .SH AUTHOR 34 | Léon Keijser 35 | 36 | .SH "SEE ALSO" 37 | .RI stonevpn (1) 38 | 39 | -------------------------------------------------------------------------------- /rpm/stonevpn.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} 2 | 3 | Name: stonevpn 4 | Version: 0.4.7 5 | Release: 1%{?dist} 6 | Summary: Easy OpenVPN certificate and configuration management 7 | 8 | Group: Applications/Internet 9 | License: GPLv2+ 10 | URL: http://github.com/lkeijser/stonevpn 11 | Source0: http://cloud.github.com/downloads/lkeijser/%{name}/%{name}-%{version}.tar.gz 12 | 13 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) 14 | BuildArch: noarch 15 | 16 | BuildRequires: python-devel 17 | Requires: python-configobj python-IPy pyOpenSSL 18 | 19 | %description 20 | StoneVPN allows you to manage OpenVPN certificates and create 21 | configurations for Windows and Linux machines based on a 22 | template. It can package everything into a zipfile and mail 23 | it to a user. 24 | 25 | %prep 26 | %setup -q 27 | 28 | %build 29 | %{__python} setup.py build 30 | 31 | %install 32 | %{__rm} -rf %{buildroot} 33 | %{__python} setup.py install --root %{buildroot} 34 | mkdir -p %{buildroot}/%{_mandir}/man{1,5} 35 | install -m 644 man/stonevpn.1 %{buildroot}/%{_mandir}/man1/ 36 | install -m 644 man/stonevpn.conf.5 %{buildroot}/%{_mandir}/man5/ 37 | 38 | %clean 39 | %{__rm} -rf %{buildroot} 40 | 41 | %files 42 | %defattr(-,root,root,-) 43 | %doc COPYING README TODO Changelog 44 | %dir %{python_sitelib}/StoneVPN 45 | %{python_sitelib}/StoneVPN/app.py* 46 | %{python_sitelib}/StoneVPN/__init__.py* 47 | %if 0%{?fedora} 48 | %{python_sitelib}/%{name}*.egg-info 49 | %endif 50 | %{_bindir}/stonevpn 51 | %config(noreplace) %{_sysconfdir}/%{name}.conf 52 | %{_mandir}/man1/%{name}.* 53 | %{_mandir}/man5/%{name}.* 54 | 55 | %changelog 56 | * ??? ??? ?? ???? L.S. Keijser - 0.4.8-1 57 | - new version from upstream 58 | 59 | * Fri Mar 12 2010 L.S. Keijser - 0.4.7-1 60 | - new version from upstream 61 | 62 | * Fri Feb 19 2010 L.S. Keijser - 0.4.6-1 63 | - new version from upstream 64 | 65 | * Wed Feb 03 2010 L.S. Keijser - 0.4.5-2 66 | - typo in branch tag in files section 67 | 68 | * Tue Feb 02 2010 L.S. Keijser - 0.4.5-1 69 | - new version from upstream 70 | 71 | * Mon Nov 09 2009 L.S. Keijser - 0.4.4-1 72 | - new version from upstream 73 | 74 | * Fri Nov 06 2009 L.S. Keijser - 0.4.3-2 75 | - _really_ removed unnecessary files residing in /usr/share/StoneVPN 76 | 77 | * Fri Nov 06 2009 L.S. Keijser - 0.4.3-1 78 | - fixed EVR: now set to 1 (Fedora standard) 79 | - fixed license tag 80 | - fixed SourceURL 81 | - removed unnecessary files (specfile, patches and license files) 82 | - ensure /etc/stonevpn.conf is present after installation 83 | 84 | * Thu Oct 22 2009 L.S. Keijser - 0.4.3-0 85 | - changed for Fedora packaging release testing 86 | 87 | * Sat Oct 17 2009 L.S. Keijser - 0.4.2-2 88 | - fixed all rpmlint warnings/errors 89 | - cleaned up spec file 90 | 91 | * Fri Aug 7 2009 L.S. Keijser 92 | - modify according to new way of installation 93 | 94 | * Tue Jul 14 2009 L.S. Keijser 95 | - change the way config file is installed 96 | 97 | * Tue May 19 2009 L.S. Keijser 98 | - bumped to version 0.4.1 99 | 100 | * Fri Mar 27 2009 L.S. Keijser 101 | - initial release 102 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup, Command 4 | import os, sys 5 | from StoneVPN import STONEVPN_VERSION 6 | 7 | class SetupBuildCommand(Command): 8 | user_options = [] 9 | def initialize_options(self): 10 | self._dir = os.getcwd() 11 | def finalize_options(self): 12 | pass 13 | 14 | class InstallDocsCommand(SetupBuildCommand): 15 | """ 16 | Extra command to install documentation files 17 | """ 18 | description = "install documentation and sample configuration files" 19 | def run(self): 20 | import shutil 21 | doc_files=( 22 | ('share/StoneVPN',['README','COPYING','Changelog','TODO']), 23 | ('share/StoneVPN/example',['conf/stonevpn.conf']), 24 | ('share/StoneVPN/rpm',['rpm/stonevpn.spec']), 25 | ('share/man/man1',['man/stonevpn.1']), 26 | ('share/man/man5',['man/stonevpn.conf.5']) 27 | ) 28 | for dst_path,files in doc_files: 29 | for src in files: 30 | filename = str(src.split('/')[len(src.split('/'))-1]) 31 | print "copying %s to /usr/%s/%s" % (src,dst_path,filename) 32 | if not os.path.isdir('/usr/' + str(dst_path)): 33 | os.mkdir('/usr/' + str(dst_path)) 34 | shutil.copy(src, '/usr/' + str(dst_path) + "/" + str(filename)) 35 | cmd = 'gzip /usr/share/man/man1/stonevpn.1' 36 | os.system(cmd) 37 | cmd = 'gzip /usr/share/man/man5/stonevpn.conf.5' 38 | os.system(cmd) 39 | 40 | # Generate list of files 41 | files=[] 42 | for f in os.path.abspath(''): 43 | files.append(f) 44 | 45 | setup(name = 'stonevpn', 46 | version = STONEVPN_VERSION, 47 | description = 'Easy OpenVPN certificate and configuration management', 48 | long_description = 'StoneVPN is a system that makes it easy to create certificates and configuration files for use with an OpenVPN server for Windows, Linux and Mac users. It has the ability to create a zip file and e-mail the entire package to a user. It uses pyOpenSSL, and the latest version of it allows it to manage a CRL file.', 49 | author = 'Leon Keijser', 50 | author_email = 'leon@gotlinux.nl', 51 | url = 'http://github.com/lkeijser/stonevpn/tree/master', 52 | download_url = 'http://github.com/lkeijser/stonevpn/downloads', 53 | license = 'GPLv2+', 54 | packages = ['StoneVPN'], 55 | package_data = {'stonevpn': files}, 56 | scripts = ["bin/stonevpn"], 57 | data_files=[ 58 | ('/etc',['conf/stonevpn.conf']), 59 | ], 60 | cmdclass = { 'install_docs': InstallDocsCommand } 61 | ), 62 | --------------------------------------------------------------------------------