From: <abe...@us...> - 2017-07-13 20:34:45
|
Revision: 8437 http://sourceforge.net/p/astlinux/code/8437 Author: abelbeck Date: 2017-07-13 20:34:42 +0000 (Thu, 13 Jul 2017) Log Message: ----------- Network tab, add "ACME (Let's Encrypt) Certificate" section with topic help info Modified Paths: -------------- branches/1.0/package/webinterface/altweb/admin/network.php branches/1.0/package/webinterface/altweb/admin/siptlscert.php branches/1.0/package/webinterface/altweb/admin/slapd.php branches/1.0/package/webinterface/altweb/admin/xmpp.php branches/1.0/package/webinterface/altweb/common/topics.info Modified: branches/1.0/package/webinterface/altweb/admin/network.php =================================================================== --- branches/1.0/package/webinterface/altweb/admin/network.php 2017-07-12 16:54:49 UTC (rev 8436) +++ branches/1.0/package/webinterface/altweb/admin/network.php 2017-07-13 20:34:42 UTC (rev 8437) @@ -44,6 +44,7 @@ // 01-29-2017, Added DDGETIPV6 support // 02-16-2017, Added Restart FTP Server support // 06-02-2017, Added selectable Prefix Delegation interfaces +// 07-12-2017, Added ACME (Let's Encrypt) Certificate configuration // // System location of rc.conf file $CONFFILE = '/etc/rc.conf'; @@ -474,7 +475,25 @@ $value = 'SMTP_PASS="'.string2RCconfig(trim($_POST['smtp_pass'])).'"'; fwrite($fp, "### SMTP Auth Password\n".$value."\n"); - + + $x_value = ''; + if (isset($_POST['acme_lighttpd'])) { + $x_value .= ' lighttpd'; + } + if (isset($_POST['acme_asterisk'])) { + $x_value .= ' asterisk'; + } + if (isset($_POST['acme_prosody'])) { + $x_value .= ' prosody'; + } + if (isset($_POST['acme_slapd'])) { + $x_value .= ' slapd'; + } + $value = 'ACME_SERVICE="'.trim($x_value).'"'; + fwrite($fp, "### ACME Certificate\n".$value."\n"); + $value = 'ACME_ACCOUNT_EMAIL="'.tuq($_POST['acme_account_email']).'"'; + fwrite($fp, $value."\n"); + $value = 'FTPD="'.$_POST['ftp'].'"'; fwrite($fp, "### FTP Server\n".$value."\n"); $value = 'FTPD_WRITE="'.$_POST['ftpd_write'].'"'; @@ -568,7 +587,7 @@ fwrite($fp, "### HTTPS access logging\n".$value."\n"); $value = 'HTTPSCERT="'.tuq($_POST['https_cert']).'"'; - if (isset($_POST['create_cert']) && is_opensslHERE()) { + if (isset($_POST['submit_self_signed_https']) && isset($_POST['confirm_self_signed_https'])) { if (($countryName = getPREFdef($global_prefs, 'dn_country_name_cmdstr')) === '') { $countryName = 'US'; } @@ -598,6 +617,8 @@ } } fwrite($fp, "### HTTPS Certificate File\n".$value."\n"); + $value = isset($_POST['acme_lighttpd']) ? 'HTTPSCHAIN="/mnt/kd/ssl/https_ca_chain.pem"' : 'HTTPSCHAIN=""'; + fwrite($fp, $value."\n"); $value = 'PHONEPROV_ALLOW="'.tuq($_POST['phoneprov_allow']).'"'; fwrite($fp, "### /phoneprov/ Allowed IPs\n".$value."\n"); @@ -980,7 +1001,15 @@ $result = saveNETWORKsettings($NETCONFDIR, $NETCONFFILE); header('Location: /admin/dnscrypt.php'); exit; - } elseif (isset($_POST['submit_sip_tls'])) { + } elseif (isset($_POST['submit_self_signed_https'])) { + if (isset($_POST['confirm_self_signed_https'])) { + if (($result = saveNETWORKsettings($NETCONFDIR, $NETCONFFILE)) == 11) { + $result = 12; + } + } else { + $result = 2; + } + } elseif (isset($_POST['submit_self_signed_sip_tls'])) { $result = saveNETWORKsettings($NETCONFDIR, $NETCONFFILE); header('Location: /admin/siptlscert.php'); exit; @@ -1212,6 +1241,8 @@ putHtml('<p style="color: green;">System is Rebooting... back in <span id="count_down"><script language="JavaScript" type="text/javascript">document.write(count_down_secs);</script></span> seconds.</p>'); } elseif ($result == 11) { putHtml('<p style="color: green;">Settings saved, click "Reboot/Restart" to apply any changed settings, a "Reboot System" is required for Interface changes.</p>'); + } elseif ($result == 12) { + putHtml('<p style="color: green;">Settings saved, a new Self-Signed HTTPS certificate is installed, a "Reboot System" is required to apply changes.</p>'); } elseif ($result == 21) { putHtml('<p style="color: green;">PPPoE has Restarted.</p>'); } elseif ($result == 22) { @@ -1881,8 +1912,40 @@ } putHtml('<tr class="dtrow0"><td colspan="6"> </td></tr>'); - + putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="6">'); + putHtml('<strong>ACME (Let\'s Encrypt) Certificate:</strong>'.includeTOPICinfo('ACME-Certificate')); + putHtml('</td></tr>'); + + putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); + putHtml('ACME Deploy Service:'); + $sel = isVARtype('ACME_SERVICE', $db, $cur_db, 'lighttpd') ? ' checked="checked"' : ''; + putHtml('<input type="checkbox" value="acme_lighttpd" name="acme_lighttpd"'.$sel.' /> HTTPS Server'); + $sel = isVARtype('ACME_SERVICE', $db, $cur_db, 'asterisk') ? ' checked="checked"' : ''; + putHtml('<input type="checkbox" value="acme_asterisk" name="acme_asterisk"'.$sel.' /> Asterisk SIP-TLS'); + $sel = isVARtype('ACME_SERVICE', $db, $cur_db, 'prosody') ? ' checked="checked"' : ''; + putHtml('<input type="checkbox" value="acme_prosody" name="acme_prosody"'.$sel.' /> XMPP Server'); + $sel = isVARtype('ACME_SERVICE', $db, $cur_db, 'slapd') ? ' checked="checked"' : ''; + putHtml('<input type="checkbox" value="acme_slapd" name="acme_slapd"'.$sel.' /> LDAP Server'); + putHtml('</td></tr>'); + + putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); + $value = getVARdef($db, 'ACME_ACCOUNT_EMAIL', $cur_db); + putHtml('ACME Account Email Address:<input type="text" size="36" maxlength="128" value="'.$value.'" name="acme_account_email" /></td></tr>'); + + putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); + putHtml('Non-ACME Self-Signed HTTPS Certificate:'); + putHtml('<input type="submit" value="Self-Signed HTTPS Cert" name="submit_self_signed_https" class="button" />'); + putHtml('–'); + putHtml('<input type="checkbox" value="self_signed_https" name="confirm_self_signed_https" /> Confirm</td></tr>'); + + putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); + putHtml('Non-ACME Self-Signed SIP-TLS Certificate:'); + putHtml('<input type="submit" value="Self-Signed SIP-TLS Cert" name="submit_self_signed_sip_tls" class="button" /></td></tr>'); + + putHtml('<tr class="dtrow0"><td colspan="6"> </td></tr>'); + + putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="6">'); putHtml('<strong>Network Services:</strong>'); putHtml('</td></tr>'); putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); @@ -1902,10 +1965,6 @@ } putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); - putHtml('Asterisk SIP-TLS Server Certificate:'); - putHtml('<input type="submit" value="SIP-TLS Certificate" name="submit_sip_tls" class="button" /></td></tr>'); - - putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); putHtml('XMPP Server, Messaging and Presence:'); putHtml('<input type="submit" value="Configure XMPP" name="submit_xmpp" class="button" /></td></tr>'); @@ -2068,15 +2127,8 @@ putHtml('</td></tr>'); $value = getVARdef($db, 'HTTPSCERT', $cur_db); - if (is_opensslHERE()) { - putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="4">'); - putHtml('HTTPS Certificate File:<input type="text" size="36" maxlength="64" value="'.$value.'" name="https_cert" /></td>'); - putHtml('<td style="text-align: left;" colspan="2">'); - putHtml('<input type="checkbox" value="create_cert" name="create_cert" /> Create New HTTPS Certificate'); - } else { - putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); - putHtml('HTTPS Certificate File:<input type="text" size="36" maxlength="64" value="'.$value.'" name="https_cert" />'); - } + putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); + putHtml('HTTPS Certificate File:<input type="text" size="36" maxlength="64" value="'.$value.'" name="https_cert" />'); putHtml('</td></tr>'); putHtml('<tr class="dtrow1"><td style="text-align: left;" colspan="6">'); Modified: branches/1.0/package/webinterface/altweb/admin/siptlscert.php =================================================================== --- branches/1.0/package/webinterface/altweb/admin/siptlscert.php 2017-07-12 16:54:49 UTC (rev 8436) +++ branches/1.0/package/webinterface/altweb/admin/siptlscert.php 2017-07-13 20:34:42 UTC (rev 8437) @@ -1,6 +1,6 @@ <?php -// Copyright (C) 2008-2012 Lonnie Abelbeck +// Copyright (C) 2008-2017 Lonnie Abelbeck // This is free software, licensed under the GNU General Public License // version 3 as published by the Free Software Foundation; you can // redistribute it and/or modify it under the terms of the GNU @@ -9,6 +9,7 @@ // siptlscert.php for AstLinux // 11-12-2012 // 12-14-2015, Added Signature Algorithm support +// 07-12-2017, Added ACME warning // // System location of /mnt/kd/rc.conf.d directory $SIPTLSCERTCONFDIR = '/mnt/kd/rc.conf.d'; @@ -271,7 +272,7 @@ <form id="iform" method="post" action="<?php echo $myself;?>"> <table width="100%" class="stdtable"> <tr><td style="text-align: center;" colspan="2"> - <h2>Asterisk SIP-TLS Server Certificate:</h2> + <h2>Self-Signed SIP-TLS Server Certificate:</h2> </td></tr><tr><td width="240" style="text-align: center;"> <input type="submit" class="formbtn" value="Save Settings" name="submit_save" /> </td><td class="dialogText" style="text-align: center;"> @@ -280,6 +281,14 @@ <table class="stdtable"> <tr class="dtrow0"><td width="140"> </td><td width="50"> </td><td width="100"> </td><td> </td><td width="100"> </td><td width="80"> </td></tr> <?php +if (is_dir('/mnt/kd/acme')) { + putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="6">'); + putHtml('<strong>ACME (Let\'s Encrypt) Certificate Exists!</strong>'); + putHtml('</td></tr>'); + + putHtml('<tr class="dtrow1"><td style="color: red; text-align: center;" colspan="6">'); + putHtml('Warning: "Create New" may overwrite deployed ACME credentials.</td></tr>'); +} if ($openssl !== FALSE) { putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="6">'); putHtml('<strong>Server Certificate and Key:</strong>'); Modified: branches/1.0/package/webinterface/altweb/admin/slapd.php =================================================================== --- branches/1.0/package/webinterface/altweb/admin/slapd.php 2017-07-12 16:54:49 UTC (rev 8436) +++ branches/1.0/package/webinterface/altweb/admin/slapd.php 2017-07-13 20:34:42 UTC (rev 8437) @@ -1,6 +1,6 @@ <?php -// Copyright (C) 2013 Lonnie Abelbeck +// Copyright (C) 2013-2017 Lonnie Abelbeck // This is free software, licensed under the GNU General Public License // version 3 as published by the Free Software Foundation; you can // redistribute it and/or modify it under the terms of the GNU @@ -120,7 +120,7 @@ } else { $result = 2; } - } elseif (isset($_POST['submit_sip_tls'])) { + } elseif (isset($_POST['submit_self_signed_sip_tls'])) { $result = saveSLAPDsettings($SLAPDCONFDIR, $SLAPDCONFFILE); header('Location: /admin/siptlscert.php'); exit; @@ -186,15 +186,19 @@ <table class="stdtable"> <tr class="dtrow0"><td width="60"> </td><td width="100"> </td><td width="50"> </td><td> </td><td> </td><td width="60"> </td></tr> <?php -if (! is_file('/mnt/kd/ssl/sip-tls/keys/server.crt') || ! is_file('/mnt/kd/ssl/sip-tls/keys/server.key')) { +if ((! is_file('/mnt/kd/ssl/sip-tls/keys/server.crt') || ! is_file('/mnt/kd/ssl/sip-tls/keys/server.key')) && + (! is_file('/mnt/kd/ldap/certs/server.crt') || ! is_file('/mnt/kd/ldap/certs/server.key'))) { putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="6">'); - putHtml('<strong>Missing SIP-TLS Server Certificate:</strong> <i>(Shared with LDAP Server)</i>'); + putHtml('<strong>Missing Server Certificate!</strong>'); putHtml('</td></tr>'); + putHtml('<tr class="dtrow1"><td style="text-align: center;" colspan="6">'); + putHtml('How to Issue an ACME (Let\'s Encrypt) Certificate:'.includeTOPICinfo('ACME-Certificate')); + putHtml('</td></tr>'); putHtml('<tr class="dtrow1"><td style="text-align: right;" colspan="2">'); - putHtml('Create SIP-TLS<br />Server Certificate:'); + putHtml('Non-ACME SIP-TLS<br />Server Certificate:'); putHtml('</td><td style="text-align: left;" colspan="4">'); - putHtml('<input type="submit" value="SIP-TLS Certificate" name="submit_sip_tls" class="button" />'); + putHtml('<input type="submit" value="Self-Signed SIP-TLS Cert" name="submit_self_signed_sip_tls" class="button" />'); putHtml('</td></tr>'); } Modified: branches/1.0/package/webinterface/altweb/admin/xmpp.php =================================================================== --- branches/1.0/package/webinterface/altweb/admin/xmpp.php 2017-07-12 16:54:49 UTC (rev 8436) +++ branches/1.0/package/webinterface/altweb/admin/xmpp.php 2017-07-13 20:34:42 UTC (rev 8437) @@ -1,6 +1,6 @@ <?php -// Copyright (C) 2013-2016 Lonnie Abelbeck +// Copyright (C) 2013-2017 Lonnie Abelbeck // This is free software, licensed under the GNU General Public License // version 3 as published by the Free Software Foundation; you can // redistribute it and/or modify it under the terms of the GNU @@ -294,7 +294,7 @@ if (reloadModule('groups') === TRUE) { $result = 16; } - } elseif (isset($_POST['submit_sip_tls'])) { + } elseif (isset($_POST['submit_self_signed_sip_tls'])) { $result = saveXMPPsettings($XMPPCONFDIR, $XMPPCONFFILE); header('Location: /admin/siptlscert.php'); exit; @@ -373,15 +373,19 @@ putHtml('<tr class="dtrow0"><td width="180"> </td><td> </td></tr>'); if ($global_admin) { -if (! is_file('/mnt/kd/ssl/sip-tls/keys/server.crt') || ! is_file('/mnt/kd/ssl/sip-tls/keys/server.key')) { +if ((! is_file('/mnt/kd/ssl/sip-tls/keys/server.crt') || ! is_file('/mnt/kd/ssl/sip-tls/keys/server.key')) && + (! is_file('/mnt/kd/prosody/certs/server.crt') || ! is_file('/mnt/kd/prosody/certs/server.key'))) { putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="2">'); - putHtml('<strong>Missing SIP-TLS Server Certificate:</strong> <i>(Shared with XMPP)</i>'); + putHtml('<strong>Missing Server Certificate!</strong>'); putHtml('</td></tr>'); + putHtml('<tr class="dtrow1"><td style="text-align: center;" colspan="2">'); + putHtml('How to Issue an ACME (Let\'s Encrypt) Certificate:'.includeTOPICinfo('ACME-Certificate')); + putHtml('</td></tr>'); putHtml('<tr class="dtrow1"><td style="text-align: right;">'); - putHtml('Create SIP-TLS<br />Server Certificate:'); + putHtml('Non-ACME SIP-TLS<br />Server Certificate:'); putHtml('</td><td style="text-align: left;">'); - putHtml('<input type="submit" value="SIP-TLS Certificate" name="submit_sip_tls" class="button" />'); + putHtml('<input type="submit" value="Self-Signed SIP-TLS Cert" name="submit_self_signed_sip_tls" class="button" />'); putHtml('</td></tr>'); } putHtml('<tr class="dtrow0"><td class="dialogText" style="text-align: left;" colspan="2">'); Modified: branches/1.0/package/webinterface/altweb/common/topics.info =================================================================== --- branches/1.0/package/webinterface/altweb/common/topics.info 2017-07-12 16:54:49 UTC (rev 8436) +++ branches/1.0/package/webinterface/altweb/common/topics.info 2017-07-13 20:34:42 UTC (rev 8437) @@ -448,3 +448,150 @@ Options: hex_revision_num revert given FILE back to given REVISION +[[ACME-Certificate]] + +--------------------------------- +ACME (Let's Encrypt) Certificates +--------------------------------- +AstLinux uses the "acme-client" command as a front-end to the core acme.sh script provided by the https://github.com/Neilpang/acme.sh project. + +The acme-client command limits issued certificates to only use DNS challenge validation, as such you need a supported DNS provider, of which there are well over 20 as of this writing. + +The Command Line Interface (CLI) must be used to initially issue and deploy ACME certificates. + + +------------------ +ACME Configuration +------------------ +Use the web interface "Network tab -> ACME (Let's Encrypt) Certificate:" section to define which services will be deployed ACME certificates. + +The "ACME Account Email Address" registration email address is used for expiry notifications, while optional it seems like a good idea to specify. + +In order to apply web interface settings changes, use the CLI command: + +CLI> gen-rc-conf + + +-------------------- +Issuing Certificates +-------------------- +This example on host pbx4 uses the acme-client command, the core acme.sh version can be obtained by issuing: + +CLI> acme-client --version +https://github.com/Neilpang/acme.sh +v2.7.2 + +Only DNS challenge validation is supported within AstLinux, as such you need a supported DNS provider, in this example we are using Cloudflare. We need to export the CF_Key and CF_Email variables, adjust to match your credentials ... + +CLI> export CF_Key="sdfdxxxxxxxosdfgje" +CLI> export CF_Email="em...@ex..." + +Other DNS providers require different exported variables, see the acme.sh documentation for the details. + +Now for the fundamental CLI command, where we issue a new certificate for the single domain "pbx4.example.org" ... + +CLI> acme-client --issue --dns dns_cf -d pbx4.example.org +[Sat Jul 1 10:08:04 CDT 2017] Registering account +[Sat Jul 1 10:08:06 CDT 2017] Registered +[Sat Jul 1 10:08:06 CDT 2017] Update success. +[Sat Jul 1 10:08:06 CDT 2017] ACCOUNT_THUMBPRINT='...' +[Sat Jul 1 10:08:06 CDT 2017] Creating domain key +[Sat Jul 1 10:08:07 CDT 2017] The domain key is here: /mnt/kd/acme/pbx4.example.org/pbx4.example.org.key +[Sat Jul 1 10:08:07 CDT 2017] Single domain='pbx4.example.org' +[Sat Jul 1 10:08:07 CDT 2017] Getting domain auth token for each domain +[Sat Jul 1 10:08:07 CDT 2017] Getting webroot for domain='pbx4.example.org' +[Sat Jul 1 10:08:07 CDT 2017] Getting new-authz for domain='pbx4.example.org' +[Sat Jul 1 10:08:07 CDT 2017] The new-authz request is ok. +[Sat Jul 1 10:08:08 CDT 2017] Found domain api file: /stat/etc/acme/dnsapi/dns_cf.sh +[Sat Jul 1 10:08:09 CDT 2017] Adding record +[Sat Jul 1 10:08:09 CDT 2017] Added, OK +[Sat Jul 1 10:08:09 CDT 2017] Sleep 120 seconds for the txt records to take effect + +[Sat Jul 1 10:10:11 CDT 2017] Verifying:pbx4.example.org +[Sat Jul 1 10:10:14 CDT 2017] Success +[Sat Jul 1 10:10:16 CDT 2017] Verify finished, start to sign. +[Sat Jul 1 10:10:16 CDT 2017] Cert success. +-----BEGIN CERTIFICATE----- +... snip ... +-----END CERTIFICATE----- +[Sat Jul 1 10:10:16 CDT 2017] Your cert is in /mnt/kd/acme/pbx4.example.org/pbx4.example.org.cer +[Sat Jul 1 10:10:16 CDT 2017] Your cert key is in /mnt/kd/acme/pbx4.example.org/pbx4.example.org.key +[Sat Jul 1 10:10:17 CDT 2017] The intermediate CA cert is in /mnt/kd/acme/pbx4.example.org/ca.cer +[Sat Jul 1 10:10:17 CDT 2017] And the full chain certs is there: /mnt/kd/acme/pbx4.example.org/fullchain.cer + +After the certificates are issued, they need to be deployed to the various services that can utilize them. +In this example only "HTTPS Server" is checked after "ACME Deploy Service:" in the web interface. + +CLI> acme-client --deploy --deploy-hook astlinux -d pbx4.example.org +Stopping lighttpd... +Starting lighttpd... +acme-client: New ACME certificates deployed for HTTPS and 'lighttpd' restarted +[Sat Jul 1 10:14:10 CDT 2017] Success + +While not required, it is a good idea to unset the exported variables above that contain the DNS challenge validation credentials. + +CLI> unset CF_Key +CLI> unset CF_Email + +NOTE: The DNS challenge validation credentials remain stored in the /mnt/kd/acme/account.conf file so auto-renewals can be performed via cron. + + +-------------------------- +Auto-Renewing Certificates +-------------------------- +Let's Encrypt certificates are only valid for 90 days, renewable after 60 days from the issue date. As such it is important to automate the process of renewing the certificate, this can be done by installing a cron entry using the command: + +CLI> acme-client --install-cronjob +acme-client: Successfully added cron entry. + + +---------------------------- +Multiple Domain Certificates +---------------------------- +In the example above only one domain pbx4.example.org was specified. Let's Encrypt allows multiple domains to be specified with valid "Subject Alternative Name" entries in a single certificate. This assumes the DNS A and/or AAAA and/or SRV record of each domain points to the server with the issued certificate. + +As an additional example let's say both example.org and subdomain pbx4.example.org are valid DNS entries you want to include in the "Subject Alternative Name" of the issued certificate. + +Proceed as above, but simply include -d example.org when issuing the certificate, (specify the more general domain first) ... + +CLI> acme-client --issue --dns dns_cf -d example.org -d pbx4.example.org + +Likewise, when deploying the certificate, though you only need to specify the first -d example.org domain ... + +CLI> acme-client --deploy --deploy-hook astlinux -d example.org + + +----------------------- +Additional CLI Commands +----------------------- +Some additional commands that may be useful to know ... + +List the issued certificate(s): + +CLI> acme-client --list + +Revoke an issued certificate by domain: + +CLI> acme-client --revoke -d pbx4.example.org + +Remove a certificate by domain: + +CLI> acme-client --remove -d pbx4.example.org + + +---------------- +Advanced Options +---------------- +For advanced users there may be situations where it would be useful to add special options for every occurrence of the acme-client command. Increasing the log-level and defining a log file would be one such example. + +The /mnt/kd/acme/account.opts file does not exist by default, and needs to be manually created to enable this feature. + +Example /mnt/kd/acme/account.opts file with persistent options added by acme-client to the acme.sh script: +-- /mnt/kd/acme/account.opts -- +## acme.sh options + +log-level 3 +log /var/log/acme-client.log +-- + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |