├── js ├── index.html ├── bootstrap.min.js └── jquery.min.js ├── css ├── index.html └── ssl.css ├── fonts ├── index.html ├── glyphicons-halflings-regular.ttf └── glyphicons-halflings-regular.woff ├── functions ├── debug.php ├── variables.php ├── helpers.php ├── domains.php ├── pre_check.php ├── remove_check.php ├── add_check.php ├── certs.php └── email.php ├── inc ├── form.php ├── footer.php ├── intro.php ├── header.php └── faq.php ├── index.php ├── confirm.php ├── README.md ├── unsubscribe.php ├── add.php ├── cron.php └── LICENSE.txt /js/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fonts/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/certificate-expiry-monitor/master/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RaymiiOrg/certificate-expiry-monitor/master/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /functions/debug.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function pre_dump($var) { 18 | echo "
";
19 |   var_dump($var);
20 |   echo "
"; 21 | } 22 | 23 | 24 | 25 | ?> -------------------------------------------------------------------------------- /css/ssl.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2015 Remy van Elst 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU Affero General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU Affero General Public License for more details. 13 | 14 | You should have received a copy of the GNU Affero General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | .footer { 19 | position: absolute; 20 | bottom: 0; 21 | width: 100%; 22 | padding-top: 5px; 23 | /* Set the fixed height of the footer here */ 24 | border-top: 1px gray dashed; 25 | height: 35px; 26 | background-color: #fff; 27 | } -------------------------------------------------------------------------------- /inc/form.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Please enter the domain(s) you want to monitor for certificate expiry. You can add max. 20 domains at once.

4 |
5 | 6 |
7 | 8 |
9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | -------------------------------------------------------------------------------- /inc/footer.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | ?> 18 | 19 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /inc/intro.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | ?> 18 | 19 |

SSL Certificates expire within a certain timeframe. Most of the time it is one year, sometimes it is longer or shorter.
Do you remember all the certificates you have and when you've bought them? Probably not.

20 | 21 |

This tool will help you remember when your certificates expire. Enter one or more websites below, we'll then monitor these sites and notify you a few times before they expire.
This way, you'll never forget to renew your certificates.

22 | 23 |

This is open source software. If you encounter any issues, please report them here. 24 | 25 |


26 | 27 | 28 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | 18 | error_reporting(E_ALL & ~E_NOTICE); 19 | ob_start(); 20 | $write_cache = 0; 21 | 22 | foreach (glob("functions/*.php") as $filename) { 23 | require($filename); 24 | } 25 | 26 | require('inc/header.php'); 27 | 28 | echo "
"; 29 | require('inc/intro.php'); 30 | echo "
"; 31 | 32 | echo "
"; 33 | require('inc/form.php'); 34 | echo "
"; 35 | 36 | echo "
"; 37 | require('inc/faq.php'); 38 | echo "
"; 39 | 40 | echo "
"; 41 | require('inc/footer.php'); 42 | 43 | echo ""; 44 | echo ""; 45 | echo ""; 46 | ?> 47 | 48 | 49 | -------------------------------------------------------------------------------- /functions/variables.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | $version = 1.4; 18 | $title = "Certificate Expiry Monitor"; 19 | 20 | $current_folder = get_current_folder(); 21 | 22 | # timeout in seconds 23 | $timeout = 2; 24 | 25 | date_default_timezone_set('UTC'); 26 | 27 | ini_set('default_socket_timeout', 2); 28 | 29 | $random_blurp = rand(1000,99999); 30 | 31 | $current_domain = "certificatemonitor.org"; 32 | $current_link = $current_domain; 33 | 34 | // set this to a location outside of your webroot so that it cannot be accessed via the internets. 35 | 36 | 37 | $pre_check_file = '/var/www/certificatemonitor.org/cert-monitor/pre_checks.json'; 38 | $check_file = '/var/www/certificatemonitor.org/cert-monitor/checks.json'; 39 | $deleted_check_file = '/var/www/certificatemonitor.org/cert-monitor/deleted_checks.json'; 40 | 41 | ?> -------------------------------------------------------------------------------- /inc/header.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | ?> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <?php echo $title;?> 26 | 27 | 28 | 29 | 30 | 31 | "; 34 | echo "
"; 35 | echo "
"; 36 | 37 | echo "
"; 40 | 41 | ?> -------------------------------------------------------------------------------- /confirm.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | error_reporting(E_ALL & ~E_NOTICE); 18 | foreach (glob("functions/*.php") as $filename) { 19 | require($filename); 20 | } 21 | 22 | require('inc/header.php'); 23 | 24 | if ( isset($_GET['id']) && !empty($_GET['id']) ) { 25 | $id = htmlspecialchars($_GET['id']); 26 | $uuid_pattern = "/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/"; 27 | if (preg_match($uuid_pattern, $id)) { 28 | $userip = $_SERVER["HTTP_X_FORWARDED_FOR"] ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SERVER["REMOTE_ADDR"]; 29 | $add_domain = add_domain_check($id, $userip); 30 | if (is_array($add_domain["errors"]) && count($add_domain["errors"]) != 0) { 31 | $errors = array_unique($add_domain["errors"]); 32 | foreach ($add_domain["errors"] as $key => $err_value) { 33 | echo ""; 36 | } 37 | } else { 38 | echo ""; 41 | } 42 | } else { 43 | echo ""; 47 | } 48 | } else { 49 | echo ""; 53 | } 54 | 55 | require('inc/faq.php'); 56 | 57 | require('inc/footer.php'); 58 | 59 | ?> 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Certificate Expiry Monitor 2 | 3 | Notice: https://raymii.org/s/blog/Cancellation_notice_for_cipherlist_ssldecoder_and_certificatemonitor.html 4 | 5 | ## About 6 | 7 | Certificate Expiry Monitor is an open source monitoring tool for certificates. It monitors websites and emails you when the certificates are about to expire. 8 | 9 | See the example site: https://certificatemonitor.org/ 10 | 11 | ## Requirements 12 | 13 | - PHP 5.6+ 14 | - OpenSSL 15 | - PHP must allow remote fopen. 16 | 17 | ## Installation 18 | 19 | Unpack, change some variables, setup a cronjob and go! 20 | 21 | First get the code and unpack it to your webroot: 22 | 23 | cd /var/www/html/ 24 | git clone https://github.com/RaymiiOrg/certificate-expiry-monitor.git 25 | 26 | Create the database files, outside of your webroot. If you create these inside your webroot, everybody can read them. 27 | 28 | echo '{}' > /var/www/certificate-expiry-monitor-db/pre_checks.json 29 | echo '{}' > /var/www/certificate-expiry-monitor-db/checks.json 30 | echo '{}' > /var/www/certificate-expiry-monitor-db/deleted_checks.json 31 | chown -R $wwwuser /var/www/certificate-expiry-monitor-db/*.json 32 | 33 | These files are used by the tool as database for checks. 34 | 35 | 36 | Change the location of these files in `variables.php`: 37 | 38 | 39 | // set this to a location outside of your webroot so that it cannot be accessed via the internets. 40 | 41 | $pre_check_file = '/var/www/html/certificate-expiry-monitor/pre_checks.json'; 42 | $check_file = '/var/www/html/certificate-expiry-monitor/checks.json'; 43 | $deleted_check_file = '/var/www/html/certificate-expiry-monitor/deleted_checks.json'; 44 | 45 | Also change the `$current_domain` variable, it is used in all the email addresses. 46 | 47 | $current_domain = "certificatemonitor.org"; 48 | 49 | And `$current_link`, which may or may not be the same. It is used in the confirm and unsubscribe links, and depends on your webserver configuration. `example.com/subdir` here means your unsubscribe links will start `https://example.com/subdir/unsubscribe.php`. 50 | 51 | $current_link = "certificatemonitor.org"; 52 | 53 | Set up the cronjob to run once a day: 54 | 55 | # /etc/cron.d/certificate-expiry-monitor 56 | 1 1 * * * $wwwuser /path/to/php /var/www/html/certificate-expiry-monitor/cron.php >> /var/log/certificate-expiry-monitor.log 2>&1 57 | 58 | 59 | The default timeout for checks is 2 seconds. If this is too fast for your internal services, this can be raised in the `variables.php` file. 60 | 61 | -------------------------------------------------------------------------------- /unsubscribe.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | error_reporting(E_ALL & ~E_NOTICE); 18 | foreach (glob("functions/*.php") as $filename) { 19 | require($filename); 20 | } 21 | 22 | require('inc/header.php'); 23 | 24 | 25 | if ( isset($_GET['id']) && !empty($_GET['id']) ) { 26 | $id = htmlspecialchars($_GET['id']); 27 | $userip = $_SERVER["HTTP_X_FORWARDED_FOR"] ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SERVER["REMOTE_ADDR"]; 28 | if ( isset($_GET['cron']) && !empty($_GET['cron']) ) { 29 | $cron = htmlspecialchars($_GET['cron']); 30 | } 31 | if ($cron == "auto") { 32 | $userip = "Removed automatically because too many errors occured."; 33 | } 34 | $uuid_pattern = "/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/"; 35 | if (preg_match($uuid_pattern, $id)) { 36 | $remove_domain = remove_domain_check($id, $userip); 37 | if (is_array($remove_domain["errors"]) && count($remove_domain["errors"]) != 0) { 38 | $errors = array_unique($remove_domain["errors"]); 39 | foreach ($remove_domain["errors"] as $key => $err_value) { 40 | echo ""; 43 | } 44 | } else { 45 | echo ""; 48 | } 49 | } else { 50 | echo ""; 54 | } 55 | } else { 56 | echo ""; 60 | } 61 | 62 | echo "
"; 63 | require('inc/faq.php'); 64 | echo "
"; 65 | 66 | require('inc/footer.php'); 67 | 68 | ?> 69 | -------------------------------------------------------------------------------- /functions/helpers.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | 18 | function utf8encodeNestedArray($arr) { 19 | // json_encode fails with binary data. utf-8 encode that first, some ca's like to encode images in their OID's (verisign, 1.3.6.1.5.5.7.1.12)... 20 | $encoded_arr = array(); 21 | foreach ($arr as $key => $value) { 22 | if (is_array($value)) { 23 | $encoded_arr[utf8_encode($key)] = utf8encodeNestedArray($value); 24 | } else { 25 | $encoded_arr[utf8_encode($key)] = utf8_encode($value); 26 | } 27 | } 28 | return $encoded_arr; 29 | } 30 | 31 | function startsWith($haystack, $needle) { 32 | // search backwards starting from haystack length characters from the end 33 | return $needle === "" || strrpos($haystack, $needle, -strlen($haystack)) !== FALSE; 34 | } 35 | function endsWith($haystack, $needle) { 36 | // search forward starting from end minus needle length characters 37 | if(!empty($haystack)) { 38 | return $needle === "" || strpos($haystack, $needle, strlen($haystack) - strlen($needle)) !== FALSE; 39 | } 40 | } 41 | 42 | function get_current_folder(){ 43 | $url = $_SERVER['REQUEST_URI']; 44 | $parts = explode('/',$url); 45 | $folder = ''; 46 | for ($i = 0; $i < count($parts) - 1; $i++) { 47 | $folder .= $parts[$i] . "/"; 48 | } 49 | return $folder; 50 | } 51 | 52 | function gen_uuid() { 53 | return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', 54 | // 32 bits for "time_low" 55 | mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), 56 | 57 | // 16 bits for "time_mid" 58 | mt_rand( 0, 0xffff ), 59 | 60 | // 16 bits for "time_hi_and_version", 61 | // four most significant bits holds version number 4 62 | mt_rand( 0, 0x0fff ) | 0x4000, 63 | 64 | // 16 bits, 8 bits for "clk_seq_hi_res", 65 | // 8 bits for "clk_seq_low", 66 | // two most significant bits holds zero and one for variant DCE1.1 67 | mt_rand( 0, 0x3fff ) | 0x8000, 68 | 69 | // 48 bits for "node" 70 | mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ) 71 | ); 72 | } 73 | 74 | function bcdechex($dec) { 75 | $hex = ''; 76 | do { 77 | $last = bcmod($dec, 16); 78 | $hex = dechex($last).$hex; 79 | $dec = bcdiv(bcsub($dec, $last), 16); 80 | } while($dec>0); 81 | return $hex; 82 | } 83 | 84 | ?> -------------------------------------------------------------------------------- /inc/faq.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | ?> 18 | 19 |
20 | 21 |

FAQ

22 | 23 |

Is this service free?

24 |

Yes, this service is free. You can add as many domains as you like, however, don't make it excessive. If you do, we might contact you to make an arrangement.


25 | 26 |

How often do you check a cert?

27 |

The check will run at least once every 2 days, but most of the time daily.


28 | 29 |

When will you email me?

30 |

We will email you on the following events:
31 |

    32 |
  • When you sign up, to confirm the domain(s).
  • 33 |
  • If a certificate expires in:
  • 34 |
      35 |
    • 90 days (3 months)
    • 36 |
    • 60 days (2 months)
    • 37 |
    • 30 days (1 month)
    • 38 |
    • 14 days (2 weeks)
    • 39 |
    • 7 days (1 week)
    • 40 |
    • 5 days
    • 41 |
    • 3 days
    • 42 |
    • 2 days
    • 43 |
    • 1 day
    • 44 |
    45 |
  • The day your certificate expires.
  • 46 |
  • 2 days after your certificated expired, and has not been replaced yet.
  • 47 |
  • 7 days after your certificated expired, and has not been replaced yet.
  • 48 |
  • If we cannot connect to your site.
  • 49 |
  • If we cannot connect to your site for 7 days in a row, we'll delete the check.
  • 50 |
51 | If you replace your certificate before it expires, we'll stop emailing you until the new certificate expires again.
52 | We will never spam you or sell your data to a third party.
53 |


54 | 55 | 56 |

Do you check all certificates in the chain?

57 |

Yes. All certificates in the chain are checked, a maximum of 10. You will receive notification if any of the chain certificates expire as well.


58 | 59 |

Do you provide any guarantees on uptime?

60 |

We provide this service on a best effort basis. The project is fully open source, you can set up your own instance if you demand 100% uptime.


61 | 62 |

What license is the project under?

63 |

GNU Affero GPL v3 or later.


64 | 65 |

Do you have any tips for secure certificate configuration?

66 |

Yes. You can check out Cipherli.st for secure server settings and guides. You can also use the SSL Decoder to check your current setup.


67 | 68 |

69 |


-------------------------------------------------------------------------------- /add.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | error_reporting(E_ALL & ~E_NOTICE); 18 | foreach (glob("functions/*.php") as $filename) { 19 | require($filename); 20 | } 21 | 22 | require('inc/header.php'); 23 | 24 | echo "
"; 25 | 26 | if ( isset($_POST['email']) && !empty($_POST['email']) && isset($_POST['domains']) && !empty($_POST['domains']) ) { 27 | 28 | $errors = array(); 29 | if (validate_email($_POST['email'])) { 30 | $email = htmlspecialchars($_POST['email']); 31 | } else { 32 | $errors[] = "Invalid email address."; 33 | } 34 | 35 | $domains = validate_domains($_POST['domains']); 36 | if ( count($domains['errors']) >= 1 ) { 37 | foreach ($domains['errors'] as $key => $value) { 38 | $errors[] = $value; 39 | } 40 | } 41 | 42 | if (is_array($errors) && count($errors) != 0) { 43 | $errors = array_unique($errors); 44 | foreach ($errors as $key => $value) { 45 | echo ""; 48 | } 49 | echo "Please return and try again.
"; 50 | } elseif ( is_array($errors) && count($errors) == 0 && is_array($domains['domains']) && count($domains['domains']) != 0 && count($domains['domains']) < 21) { 51 | echo ""; 54 | foreach ($domains['domains'] as $key => $value) { 55 | $userip = $_SERVER["HTTP_X_FORWARDED_FOR"] ? $_SERVER["HTTP_X_FORWARDED_FOR"] : $_SERVER["REMOTE_ADDR"]; 56 | $add_domain = add_domain_to_pre_check($value, $email, $userip); 57 | if (is_array($add_domain["errors"]) && count($add_domain["errors"]) != 0) { 58 | $errors = array_unique($add_domain["errors"]); 59 | foreach ($add_domain["errors"] as $key => $err_value) { 60 | echo ""; 63 | } 64 | } else { 65 | echo ""; 68 | } 69 | } 70 | } else { 71 | echo ""; 75 | } 76 | } else { 77 | 78 | echo ""; 82 | } 83 | 84 | 85 | require('inc/faq.php'); 86 | 87 | require('inc/footer.php'); 88 | 89 | ?> 90 | -------------------------------------------------------------------------------- /functions/domains.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function validate_domains($domains) { 18 | $errors = array(); 19 | $domains = explode("\n", $domains); 20 | $domains = array_map('strtolower', $domains); 21 | $domains = array_filter($domains); 22 | $domains = array_unique($domains); 23 | 24 | foreach ($domains as $key => $value) { 25 | $value = trim(strtolower($value)); 26 | // check if reasonably valid domain 27 | if ( !preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $value) && !preg_match("/^.{1,253}$/", $value) && !preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $value) ) { 28 | $errors[] = "Invalid domain name: " . htmlspecialchars($value) . "."; 29 | } 30 | 31 | // check valid dns record 32 | $ips = @dns_get_record($value, DNS_A + DNS_AAAA); 33 | if(!$ips) { 34 | $errors[] = "Error resolving domain: " . htmlspecialchars($value); 35 | continue; 36 | } 37 | sort($ips); 38 | if ( count($ips) >= 1 ) { 39 | if (!empty($ips[0]['type']) ) { 40 | if ($ips[0]['type'] === "AAAA") { 41 | $ip = $ips[0]['ipv6']; 42 | if( !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) { 43 | $errors[] = "Invalid domain AAAA record for: " . htmlspecialchars($value) . "."; 44 | } 45 | } elseif ($ips[0]['type'] === "A") { 46 | $ip = $ips[0]['ip']; 47 | if( !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) { 48 | $errors[] = "Invalid domain A record for: " . htmlspecialchars($value) . "."; 49 | } 50 | } 51 | } else { 52 | $errors[] = "No DNS A/AAAA records for: " . htmlspecialchars($value) . "."; 53 | } 54 | } else { 55 | $errors[] = "Error resolving domain: " . htmlspecialchars($value) . "."; 56 | } 57 | } 58 | 59 | if (is_array($errors) && count($errors) == 0) { 60 | foreach ($domains as $key => $value) { 61 | $raw_chain = get_raw_chain(trim($value)); 62 | if ($raw_chain['error']) { 63 | foreach ($raw_chain['error'] as $error_key => $error_value) { 64 | $errors[] = "\n - " . htmlspecialchars($error_value); 65 | } 66 | } else { 67 | foreach ($raw_chain['chain'] as $raw_key => $raw_value) { 68 | $cert_expiry = cert_expiry($raw_value); 69 | $cert_subject = cert_subject($raw_value); 70 | if ($cert_expiry['cert_expired']) { 71 | $errors[] = "Domain has expired certificate in chain: " . htmlspecialchars($value) . ". Cert Subject: " . htmlspecialchars($cert_subject) . "."; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | 79 | if (is_array($errors) && count($errors) >= 1) { 80 | $result = array(); 81 | foreach ($errors as $key => $value) { 82 | $result['errors'][] = $value; 83 | } 84 | return $result; 85 | } else { 86 | $result = array(); 87 | foreach ($domains as $key => $value) { 88 | $result['domains'][] = $value; 89 | } 90 | return $result; 91 | } 92 | } 93 | 94 | ?> -------------------------------------------------------------------------------- /functions/pre_check.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function add_domain_to_pre_check($domain,$email,$visitor_ip) { 18 | global $current_domain; 19 | global $current_link; 20 | global $pre_check_file; 21 | global $check_file; 22 | global $title; 23 | 24 | $result = array(); 25 | $domain = trim($domain); 26 | $email = trim($email); 27 | $file = file_get_contents($pre_check_file); 28 | if ($file === FALSE) { 29 | $result['errors'][] = "Can't open database."; 30 | return $result; 31 | } 32 | $json_a = json_decode($file, true); 33 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 34 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 35 | return $result; 36 | } 37 | 38 | foreach ($json_a as $key => $value) { 39 | if ($value["domain"] == $domain && $value["email"] == $email) { 40 | $result['errors'][] = "Domain/email combo for " . htmlspecialchars($domain) . " already exists. Please confirm your subscription email."; 41 | return $result; 42 | } 43 | } 44 | 45 | $check_json_file = file_get_contents($check_file); 46 | if ($check_json_file === FALSE) { 47 | $result['errors'][] = "Can't open database."; 48 | return $result; 49 | } 50 | $check_json_a = json_decode($check_json_file, true); 51 | if ($check_json_a === null && json_last_error() !== JSON_ERROR_NONE) { 52 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 53 | return $result; 54 | } 55 | 56 | foreach ($check_json_a as $key => $value) { 57 | if ($value["domain"] == $domain && $value["email"] == $email) { 58 | $result['errors'][] = "Domain / email combo for " . htmlspecialchars($domain) . " already exists."; 59 | return $result; 60 | } 61 | } 62 | 63 | $uuid = gen_uuid(); 64 | 65 | $json_a[$uuid] = array("domain" => $domain, 66 | "email" => $email, 67 | "visitor_pre_register_ip" => $visitor_ip, 68 | "pre_add_date" => time()); 69 | 70 | $json = json_encode($json_a); 71 | if(file_put_contents($pre_check_file, $json, LOCK_EX)) { 72 | $result['success'][] = true; 73 | } else { 74 | $result['errors'][] = "Can't write database."; 75 | return $result; 76 | } 77 | 78 | $sublink = "https://" . $current_link . "/confirm.php?id=" . $uuid; 79 | 80 | $to = $email; 81 | $subject = "Confirm your " . $title . " subscription for " . htmlspecialchars($domain) . "."; 82 | $message = "Hello,\r\n\r\nSomeone, hopefully you, has added his website to the Certificate Expiry Monitor. This is a service which monitors an SSL certificate on a website, and notifies you when it is about to expire. This extra notification helps you remember to renew your certificate on time.\r\n\r\nIf you have subscribed to this check, please click the link below to confirm this subscription. If you haven't subscribed to the Certificate Expiry Monitor service, please consider this message as not sent.\r\n\r\n\r\nDomain: " . trim(htmlspecialchars($domain)) . "\r\nEmail: " . trim(htmlspecialchars($email)) . "\r\nIP subscribed from: " . htmlspecialchars($visitor_ip) . "\r\nDate subscribed: " . date("Y-m-d H:i:s T") . "\r\n\r\nPlease click or copy and paste the below link in your browser to subscribe: \r\n\r\n" . $sublink . "\r\n\r\n\r\nHave a nice day,\r\nThe " . $title . " service."; 83 | $message = wordwrap($message, 70, "\r\n"); 84 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 85 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 86 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 87 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 88 | 'X-Coffee: Black' . "\r\n" . 89 | 'List-Unsubscribe: " . "\r\n" . 90 | 'X-Mailer: PHP/4.1.1'; 91 | 92 | 93 | 94 | if (mail($to, $subject, $message, $headers) === true) { 95 | $result['success'][] = true; 96 | } else { 97 | $result['errors'][] = "Can't send email."; 98 | return $result; 99 | } 100 | 101 | return $result; 102 | } 103 | -------------------------------------------------------------------------------- /functions/remove_check.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function remove_domain_check($id,$visitor_ip) { 18 | global $current_domain; 19 | global $current_link; 20 | global $check_file; 21 | global $deleted_check_file; 22 | global $title; 23 | 24 | $result = array(); 25 | 26 | $deleted_check_json_file = file_get_contents($deleted_check_file); 27 | if ($file === FALSE) { 28 | $result['errors'][] = "Can't open database."; 29 | return $result; 30 | } 31 | $deleted_check_json_a = json_decode($deleted_check_json_file, true); 32 | if ($deleted_check_json_a === null && json_last_error() !== JSON_ERROR_NONE) { 33 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 34 | return $result; 35 | } 36 | 37 | $file = file_get_contents($check_file); 38 | if ($file === FALSE) { 39 | $result['errors'][] = "Can't open database."; 40 | return $result; 41 | } 42 | $json_a = json_decode($file, true); 43 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 44 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 45 | return $result; 46 | } 47 | 48 | if (!is_array($json_a[$id]) ) { 49 | $result['errors'][] = "Can't find record in database for: " . htmlspecialchars($id); 50 | return $result; 51 | } 52 | 53 | foreach ($json_a as $key => $value) { 54 | if ($key == $id) { 55 | $deleted_json_a[$id] = array("domain" => $json_a[$id]['domain'], 56 | "email" => $json_a[$id]['email'], 57 | "visitor_pre_register_ip" => $json_a[$id]['visitor_pre_register_ip'], 58 | "pre_add_date" => $json_a[$id]['pre_add_date'], 59 | "visitor_confirm_ip" => $json_a[$id]['visitor_confirm_ip'], 60 | "confirm_date" => $json_a[$id]['confirm_date'], 61 | "visitor_delete_ip" => $visitor_ip, 62 | "delete_date" => time(), 63 | ); 64 | 65 | $deleted_json = json_encode($deleted_json_a); 66 | if(file_put_contents($deleted_check_file, $deleted_json, LOCK_EX)) { 67 | $result['success'][] = true; 68 | } else { 69 | $result['errors'][] = "Can't write database."; 70 | return $result; 71 | } 72 | 73 | unset($json_a[$id]); 74 | $check_json = json_encode($json_a); 75 | if(file_put_contents($check_file, $check_json, LOCK_EX)) { 76 | $result['success'][] = true; 77 | } else { 78 | $result['errors'][] = "Cannot write database."; 79 | return $result; 80 | } 81 | 82 | $link = "https://" . $current_link . "/"; 83 | 84 | $to = $deleted_json_a[$id]['email']; 85 | $subject = $title . " subscription removed for " . htmlspecialchars($deleted_json_a[$id]['domain']) . "."; 86 | $message = "Hello,\r\n\r\nYou have removed the subscription of a website to the " . $title . ".\r\n\r\nDomain: " . trim(htmlspecialchars($deleted_json_a[$id]['domain'])) . "\r\nEmail: " . trim(htmlspecialchars($deleted_json_a[$id]['email'])) . "\r\nIP subscription removed from: " . htmlspecialchars($visitor_ip) . "\r\nDate subscribed removed: " . date("Y-m-d H:i:s") . "\r\n\r\nWe will not monitor this website any longer and you will not receive any emails whatsoever from us again for this domain. Do note that you might miss an expiring certificate.\r\n\r\nTo re-subscribe this domain please add it again on the Certificate Expiry Monitor website: \r\n\r\n " . $link . "\r\n\r\nHave a nice day,\r\nThe " . $title . " service.\r\nhttps://" . $current_link . ""; 87 | $message = wordwrap($message, 70, "\r\n"); 88 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 89 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 90 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 91 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 92 | 'X-Coffee: Black' . "\r\n" . 93 | 'List-Unsubscribe: " . "\r\n" . 94 | 'X-Mailer: PHP/4.1.1'; 95 | 96 | if (mail($to, $subject, $message, $headers) === true) { 97 | $result['success'][] = true; 98 | } else { 99 | $result['errors'][] = "Can't send email."; 100 | return $result; 101 | } 102 | return $result; 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /functions/add_check.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function add_domain_check($id,$visitor_ip) { 18 | global $current_domain; 19 | global $current_link; 20 | global $pre_check_file; 21 | global $check_file; 22 | global $title; 23 | 24 | $result = array(); 25 | 26 | $pre_check_json_file = file_get_contents($pre_check_file); 27 | if ($pre_check_json_file === FALSE) { 28 | $result['errors'][] = "Can't open database."; 29 | return $result; 30 | } 31 | $pre_check_json_a = json_decode($pre_check_json_file, true); 32 | if ($pre_check_json_a === null && json_last_error() !== JSON_ERROR_NONE) { 33 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 34 | return $result; 35 | } 36 | 37 | if (!is_array($pre_check_json_a[$id]) ) { 38 | $result['errors'][] = "Can't find record in database for: " . htmlspecialchars($id); 39 | return $result; 40 | } 41 | 42 | $file = file_get_contents($check_file); 43 | if ($file === FALSE) { 44 | $result['errors'][] = "Can't open database."; 45 | return $result; 46 | } 47 | $json_a = json_decode($file, true); 48 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 49 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 50 | return $result; 51 | } 52 | 53 | foreach ($json_a as $key => $value) { 54 | if ($key == $id) { 55 | $result['errors'][] = "Domain/email combo for " . htmlspecialchars($pre_check_json_a[$id]['domain']) . " already exists."; 56 | return $result; 57 | } 58 | if ($value["domain"] == $pre_check_json_a[$id]['domain'] && $value["email"] == $pre_check_json_a[$id]['email']) { 59 | $result['errors'][] = "Domain / email combo for " . htmlspecialchars($pre_check_json_a[$id]['domain']) . " already exists."; 60 | return $result; 61 | } 62 | } 63 | 64 | $domains = validate_domains($pre_check_json_a[$id]['domain']); 65 | if (count($domains['errors']) >= 1 ) { 66 | $result['errors'][] = $domains['errors']; 67 | return $result; 68 | } 69 | 70 | $json_a[$id] = array("domain" => $pre_check_json_a[$id]['domain'], 71 | "email" => $pre_check_json_a[$id]['email'], 72 | "errors" => 0, 73 | "visitor_pre_register_ip" => $pre_check_json_a[$id]['visitor_pre_register_ip'], 74 | "pre_add_date" => $pre_check_json_a[$id]['pre_add_date'], 75 | "visitor_confirm_ip" => $visitor_ip, 76 | "confirm_date" => time()); 77 | 78 | $json = json_encode($json_a); 79 | if(file_put_contents($check_file, $json, LOCK_EX)) { 80 | $result['success'][] = true; 81 | } else { 82 | $result['errors'][] = "Can't write database."; 83 | return $result; 84 | } 85 | 86 | unset($pre_check_json_a[$id]); 87 | $pre_check_json = json_encode($pre_check_json_a); 88 | if(file_put_contents($pre_check_file, $pre_check_json, LOCK_EX)) { 89 | $result['success'][] = true; 90 | } else { 91 | $result['errors'][] = "Can't write database."; 92 | return $result; 93 | } 94 | 95 | $unsublink = "https://" . $current_link . "/unsubscribe.php?id=" . $id; 96 | 97 | $to = $json_a[$id]['email']; 98 | $subject = $title . " subscription confirmed for " . htmlspecialchars($json_a[$id]['domain']) . "."; 99 | $message = "Hello, 100 | 101 | Someone, hopefully you, has confirmed the subscription of their website to the " . $title . ". This is a service which monitors an SSL certificate on a website, and notifies you when it is about to expire. This extra notification helps you remember to renew your certificate on time. 102 | 103 | Domain : " . trim(htmlspecialchars($json_a[$id]['domain'])) . " 104 | Email : " . trim(htmlspecialchars($json_a[$id]['email'])) . " 105 | IP subscription confirmed from: " . htmlspecialchars($visitor_ip) . " 106 | Date subscribed confirmed: " . date("Y-m-d H:i:s T") . " 107 | 108 | We will monitor the certificates for this website. You will receive emails when it is about to expire as described in the FAQ on our website. You can view the FAQ here: https://" . $current_link . ". 109 | 110 | To unsubscribe from notifications for this domain please click or copy and paste the below link in your browser: 111 | 112 | " . $unsublink . " 113 | 114 | Have a nice day, 115 | The " . $title . " service 116 | https://" . $current_link . ""; 117 | $message = wordwrap($message, 70, "\r\n"); 118 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 119 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 120 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 121 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 122 | 'X-Coffee: Black' . "\r\n" . 123 | 'List-Unsubscribe: " . "\r\n" . 124 | 'X-Mailer: PHP/4.1.1'; 125 | 126 | 127 | 128 | if (mail($to, $subject, $message, $headers) === true) { 129 | $result['success'][] = true; 130 | } else { 131 | $result['errors'][] = "Can't send email."; 132 | return $result; 133 | } 134 | 135 | return $result; 136 | } -------------------------------------------------------------------------------- /functions/certs.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | 18 | function get_raw_chain($host,$port=443) { 19 | global $timeout; 20 | $data = []; 21 | $stream = stream_context_create (array("ssl" => 22 | array("capture_peer_cert" => true, 23 | "capture_peer_cert_chain" => true, 24 | "verify_peer" => false, 25 | "peer_name" => $host, 26 | "verify_peer_name" => false, 27 | "allow_self_signed" => true, 28 | "sni_enabled" => true))); 29 | $read_stream = @stream_socket_client("ssl://$host:$port", $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $stream); 30 | if ( $read_stream === false ) { 31 | $data["error"] = [$errstr]; 32 | return $data; 33 | } else { 34 | $context = stream_context_get_params($read_stream); 35 | $context_meta = stream_get_meta_data($read_stream)["crypto"]; 36 | $cert_data = openssl_x509_parse($context["options"]["ssl"]["peer_certificate"]); 37 | $chain_data = $context["options"]["ssl"]["peer_certificate_chain"]; 38 | $chain_length = count($chain_data); 39 | if (isset($chain_data) && $chain_length < 10) { 40 | foreach($chain_data as $key => $value) { 41 | $data["chain"][$key] = $value; 42 | } 43 | } else { 44 | $data["error"] = ["Chain too long."]; 45 | return $data; 46 | } 47 | } 48 | return $data; 49 | } 50 | 51 | function cert_expiry_date($raw_cert_data) { 52 | $cert_data = openssl_x509_parse($raw_cert_data); 53 | if (!empty($cert_data['validTo_time_t'])) { 54 | return(strtotime(date(DATE_RFC2822,$cert_data['validTo_time_t']))); 55 | } else { 56 | return false; 57 | } 58 | } 59 | 60 | function cert_valid_from($raw_cert_data) { 61 | $cert_data = openssl_x509_parse($raw_cert_data); 62 | if (!empty($cert_data['validFrom_time_t'])) { 63 | return(strtotime(date(DATE_RFC2822,$cert_data['validFrom_time_t']))); 64 | } else { 65 | return false; 66 | } 67 | } 68 | 69 | function cert_serial($raw_cert_data) { 70 | $cert_data = openssl_x509_parse($raw_cert_data); 71 | if ( isset($cert_data['serialNumber']) ) { 72 | $serial = []; 73 | $sn = str_split(strtoupper(bcdechex($cert_data['serialNumber'])), 2); 74 | $sn_len = count($sn); 75 | foreach ($sn as $key => $s) { 76 | $serial[] = htmlspecialchars($s); 77 | if ( $key != $sn_len - 1) { 78 | $serial[] = ":"; 79 | } 80 | } 81 | $result = implode("", $serial); 82 | return $result; 83 | } 84 | } 85 | 86 | function cert_cn($raw_cert_data) { 87 | $cert_data = openssl_x509_parse($raw_cert_data); 88 | if (!empty($cert_data['subject']['CN'])) { 89 | return($cert_data['subject']['CN']); 90 | } else { 91 | return false; 92 | } 93 | } 94 | 95 | function cert_subject($raw_cert_data) { 96 | $cert_data = openssl_x509_parse($raw_cert_data); 97 | if (!empty($cert_data['name'])) { 98 | return($cert_data['name']); 99 | } else { 100 | return false; 101 | } 102 | } 103 | 104 | function cert_expiry($raw_cert) { 105 | $result = array(); 106 | $today = strtotime(date("Y-m-d")); 107 | $cert_expiry_date = cert_expiry_date($raw_cert); 108 | $cert_expiry_date = strtotime(date("Y-m-d",$cert_expiry_date)); 109 | // expired 110 | if ($today < $cert_expiry_date) { 111 | $result['cert_expired'] = false; 112 | } else { 113 | $result['cert_expired'] = true; 114 | $result['cert_time_expired'] = $today - $cert_expiry_date; 115 | } 116 | if ( $result['cert_expired'] == false ) { 117 | $cert_expiry_diff = $cert_expiry_date - $today; 118 | $result['cert_time_to_expiry'] = $cert_expiry_diff; 119 | } 120 | return $result; 121 | } 122 | 123 | 124 | function cert_expiry_emails($domain, $email, $cert_expiry, $raw_cert) { 125 | if ($cert_expiry['cert_expired'] === true) { 126 | switch ($cert_expiry['cert_time_expired']) { 127 | case "0": 128 | # 0 days... 129 | send_cert_expired_email(1, $domain, $email, $raw_cert); 130 | break; 131 | case "84600": 132 | # 1 days... 133 | send_cert_expired_email(1, $domain, $email, $raw_cert); 134 | break; 135 | case "172800": 136 | # 2 days... 137 | send_cert_expired_email(2, $domain, $email, $raw_cert); 138 | break; 139 | case "604800": 140 | # 7 days... 141 | send_cert_expired_email(7, $domain, $email, $raw_cert); 142 | break; 143 | // default: 144 | // send_cert_expired_email($cert_expiry['cert_time_expired']/24/60/60, $domain, $email, $raw_cert); 145 | // break; 146 | } 147 | 148 | } else { 149 | switch ($cert_expiry['cert_time_to_expiry']) { 150 | case "7776000": 151 | # 90 days... 152 | send_expires_in_email(90, $domain, $email, $raw_cert); 153 | break; 154 | case "5184000": 155 | # 60 days... 156 | send_expires_in_email(60, $domain, $email, $raw_cert); 157 | break; 158 | case "2592000": 159 | # 30 days... 160 | send_expires_in_email(30, $domain, $email, $raw_cert); 161 | break; 162 | case "1209600": 163 | # 14 days... 164 | send_expires_in_email(14, $domain, $email, $raw_cert); 165 | break; 166 | case "604800": 167 | # 7 days... 168 | send_expires_in_email(7, $domain, $email, $raw_cert); 169 | break; 170 | case "432000": 171 | # 5 days... 172 | send_expires_in_email(5, $domain, $email, $raw_cert); 173 | break; 174 | case "259200": 175 | # 3 days... 176 | send_expires_in_email(3, $domain, $email, $raw_cert); 177 | break; 178 | case "172800": 179 | # 2 days... 180 | send_expires_in_email(2, $domain, $email, $raw_cert); 181 | break; 182 | case "86400": 183 | # 1 days... 184 | send_expires_in_email(1, $domain, $email, $raw_cert); 185 | break; 186 | case "0": 187 | # 0 days... 188 | send_expires_in_email(0, $domain, $email, $raw_cert); 189 | break; 190 | } 191 | } 192 | } 193 | 194 | ?> -------------------------------------------------------------------------------- /cron.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | error_reporting(E_ALL & ~E_NOTICE); 18 | $result = array(); 19 | if (php_sapi_name() == "cli") { 20 | foreach (glob( __DIR__ . "/functions/*.php") as $filename) { 21 | require($filename); 22 | } 23 | 24 | $removal_queue = array(); 25 | $tmp_check_file = $check_file . ".tmp"; 26 | if (!copy($check_file, $tmp_check_file)) { 27 | echo "\nFailed to copy $check_file to $tmp_check_file.\n"; 28 | die(); 29 | } 30 | 31 | $file = file_get_contents($tmp_check_file); 32 | if ($file === FALSE) { 33 | $result['errors'][] = "Can't open database."; 34 | } 35 | $json_a = json_decode($file, true); 36 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 37 | $result['errors'][] = "Can't read database: " . htmlspecialchars(json_last_error()); 38 | 39 | } 40 | 41 | if (count($json_a) == 0) { 42 | echo "\nEmpty checklist.\n"; 43 | die(); 44 | } 45 | 46 | echo "\n===== start " . date('Y-m-d H:i:s') . "=====\n"; 47 | 48 | foreach ($json_a as $key => $value) { 49 | $domain = $value['domain']; 50 | $email = $value['email']; 51 | 52 | $val_domain = validate_domains($domain); 53 | if (count($val_domain['errors']) >= 1 ) { 54 | $errors = $val_domain['errors']; 55 | $errortexts = ''; 56 | foreach ($errors as $error_value) { 57 | echo "\n" . $error_value . " Domain: " . $domain . "\n"; 58 | $errortexts .= $error_value . "\n"; 59 | send_error_mail($domain, $email, $errors); 60 | continue; 61 | } 62 | 63 | $json_a[$key]['errors'] += 1; 64 | $check_json = json_encode($json_a); 65 | if(file_put_contents($check_file, $check_json, LOCK_EX)) { 66 | echo "\nError count updated to " . $json_a[$key]['errors'] . " for " . $domain . "\n"; 67 | } else { 68 | echo "\nCan't write database.\n"; 69 | continue; 70 | } 71 | if ($json_a[$key]['errors'] >= 7) { 72 | echo "\nToo many errors. Adding " . $domain . " to removal queue.\n"; 73 | $removal_queue[] = $key; 74 | continue; 75 | } 76 | 77 | #if (strpos($errortexts,'\nDomain has expired certificate in chain') !== false) { 78 | # continue; 79 | #} 80 | } 81 | $raw_chain = get_raw_chain($domain); 82 | $counter = 0; 83 | if (count($raw_chain['chain']) > 0) { 84 | foreach ($raw_chain['chain'] as $chain_key => $chain_value) { 85 | $counter += 1; 86 | $cert_exp_date = cert_expiry_date($chain_value); 87 | $cert_cn = cert_cn($chain_value); 88 | $cert_expiry = cert_expiry($chain_value); 89 | 90 | #echo "\tCert Chain #" . $counter . ". Expiry Date: " . date("Y-m-d H:i:s T", $cert_exp_date) . ". Common Name: " . $cert_cn . "\n"; 91 | 92 | cert_expiry_emails($domain, $email, $cert_expiry, $chain_value); 93 | } 94 | 95 | } 96 | $file = file_get_contents($check_file); 97 | if ($file === FALSE) { 98 | echo "\nCan't open database.\n"; 99 | continue; 100 | } 101 | $json_a = json_decode($file, true); 102 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 103 | echo "\nCan't read database\n"; 104 | continue; 105 | } 106 | if ($json_a[$key]['errors'] != 0) { 107 | if (strpos($errortexts,'Domain has expired certificate in chain') !== false) { 108 | $json_a[$key]['errors'] = 0; 109 | $check_json = json_encode($json_a); 110 | if(file_put_contents($check_file, $check_json, LOCK_EX)) { 111 | echo "\nExpired certificate. Error count reset to 0 for " . $domain . "\n"; 112 | } else { 113 | echo "\nCan't write database.\n"; 114 | die(); 115 | } 116 | } 117 | } 118 | 119 | echo "."; 120 | 121 | } 122 | echo "\n===== end " . date('Y-m-d H:i:s') . "=====\n"; 123 | 124 | 125 | if ( count($removal_queue) != 0 ) { 126 | echo "Processing removal queue.\n"; 127 | foreach ($removal_queue as $remove_key => $remove_value) { 128 | $unsub_url = "https://" . $current_link . "/unsubscribe.php?cron=auto&id=" . $remove_value; 129 | $file = file_get_contents($unsub_url); 130 | if ($file === FALSE) { 131 | $error = error_get_last(); 132 | echo "HTTP request failed. Error was: " . $error['message']; 133 | echo "\tRemoval Error.\n"; 134 | continue; 135 | } else { 136 | echo "\tRemoved $remove_value.\n"; 137 | } 138 | } 139 | } 140 | 141 | // remove non-confirmed subs older than 7 days 142 | $tmp_pre_check_file = $pre_check_file . ".tmp"; 143 | if (!copy($pre_check_file, $tmp_pre_check_file)) { 144 | echo "Failed to copy $pre_check_file to $tmp_pre_check_file.\n"; 145 | die(); 146 | } 147 | 148 | $tmp_pre_file = file_get_contents($tmp_pre_check_file); 149 | if ($tmp_pre_file === FALSE) { 150 | echo "Can't open database.\n"; 151 | die(); 152 | } 153 | $tmp_pre_json_a = json_decode($tmp_pre_file, true); 154 | if ($tmp_pre_json_a === null && json_last_error() !== JSON_ERROR_NONE) { 155 | echo "Can't read database.\n"; 156 | die(); 157 | } 158 | 159 | if (count($tmp_pre_json_a) == 0) { 160 | echo "Empty pre-checklist.\n"; 161 | die(); 162 | } 163 | 164 | foreach ($tmp_pre_json_a as $pre_key => $pre_value) { 165 | $today = strtotime(date("Y-m-d")); 166 | $pre_add_date = strtotime(date("Y-m-d",$pre_value['pre_add_date'])); 167 | $pre_add_diff = $today - $pre_add_date; 168 | if ($pre_add_diff > "604800") { 169 | unset($tmp_pre_json_a[$pre_key]); 170 | $tmp_pre_json = json_encode($tmp_pre_json_a); 171 | if(file_put_contents($pre_check_file, $tmp_pre_json, LOCK_EX)) { 172 | echo "Subscription for " . $pre_value['domain'] . " from " . $pre_value['email'] . " older than 7 days. Removing from subscription list.\n"; 173 | } else { 174 | echo "Failed to remove subscription for " . $pre_value['domain'] . " from " . $pre_value['email'] . " older than 7 days from subscription list.\n"; 175 | } 176 | } 177 | } 178 | 179 | } else { 180 | header('HTTP/1.0 301 Moved Permanently'); 181 | header('Location: /'); 182 | } 183 | 184 | ?> 185 | -------------------------------------------------------------------------------- /functions/email.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | function validate_email($email) { 18 | if (!filter_var(strtolower($email), FILTER_VALIDATE_EMAIL)) { 19 | return false; 20 | } else { 21 | return true; 22 | } 23 | } 24 | 25 | function send_error_mail($domain, $email, $errors) { 26 | echo "\t\tSending error mail to $email for $domain.\n"; 27 | global $current_domain; 28 | global $current_link; 29 | global $check_file; 30 | global $title; 31 | 32 | $domain = trim($domain); 33 | $errors = implode("\r\n", $errors); 34 | $json_file = file_get_contents($check_file); 35 | if ($check_file === FALSE) { 36 | echo "\nCan't open database.\n"; 37 | return false; 38 | } 39 | $json_a = json_decode($json_file, true); 40 | if ($json_a === NULL || json_last_error() !== JSON_ERROR_NONE) { 41 | echo "\nCan't read database.\n"; 42 | return false; 43 | } 44 | 45 | foreach ($json_a as $key => $value) { 46 | if ($value["domain"] == $domain && $value["email"] == $email) { 47 | $id = $key; 48 | $failures = $value['errors']; 49 | $unsublink = "https://" . $current_link . "/unsubscribe.php?id=" . $id; 50 | $to = $email; 51 | $subject = "Certificate monitor " . htmlspecialchars($domain) . " failed."; 52 | $message = "Hello,\r\n\r\nYou have a subscription to monitor the certificate of " . htmlspecialchars($domain) . " with the the " . $title . ". This is a service which monitors an SSL certificate on a website, and notifies you when it is about to expire. This extra notification helps you remember to renew your certificate on time.\r\n\r\nWe've noticed that the check for the following domain has failed: \r\n\r\nDomain: " . htmlspecialchars($domain) . "\r\nError(s): " . htmlspecialchars($errors) . "\r\n\r\nFailure(s): " . htmlspecialchars($failures) . "\r\n\r\nPlease check this website or its certificate. If the check fails 7 times we will remove it from our monitoring. If the check succeeds again within 7 failures, the failure count will reset.\r\n\r\nTo unsubscribe from notifications for this domain please click or copy and paste the below link in your browser:\r\n\r\n" . $unsublink . "\r\n\r\n\r\n Have a nice day,\r\nThe " . $title . " service.\r\nhttps://" . $current_link . ""; 53 | $message = wordwrap($message, 70, "\r\n"); 54 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 55 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 56 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 57 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 58 | 'X-Coffee: Black' . "\r\n" . 59 | 'List-Unsubscribe: " . "\r\n" . 60 | 'X-Mailer: PHP/4.1.1'; 61 | 62 | if (mail($to, $subject, $message, $headers) === true) { 63 | echo "\nError mail sent to $to.\n"; 64 | return true; 65 | } else { 66 | echo "\nCan't send error email.\n"; 67 | return false; 68 | } 69 | } 70 | } 71 | } 72 | 73 | function send_cert_expired_email($days, $domain, $email, $raw_cert) { 74 | global $current_domain; 75 | global $current_link; 76 | global $check_file; 77 | global $title; 78 | $domain = trim($domain); 79 | echo "\nDomain " . $domain . " expired " . $days . " ago.\n"; 80 | 81 | $file = file_get_contents($check_file); 82 | if ($file === FALSE) { 83 | echo "\nCan't open database.\n"; 84 | return false; 85 | } 86 | $json_a = json_decode($file, true); 87 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 88 | echo "\nCan't read database.\n"; 89 | return false; 90 | } 91 | 92 | foreach ($json_a as $key => $value) { 93 | 94 | if ($value["domain"] == $domain && $value["email"] == $email) { 95 | 96 | $id = $key; 97 | $cert_cn = cert_cn($raw_cert); 98 | $cert_subject = cert_subject($raw_cert); 99 | $cert_serial = cert_serial($raw_cert); 100 | $cert_expiry_date = cert_expiry_date($raw_cert); 101 | $cert_validfrom_date = cert_valid_from($raw_cert); 102 | 103 | $now = time(); 104 | $datefromdiff = $now - $cert_validfrom_date; 105 | $datetodiff = $now - $cert_expiry_date; 106 | $cert_valid_days_ago = floor($datefromdiff/(60*60*24)); 107 | $cert_valid_days_ahead = floor($datetodiff/(60*60*24)); 108 | 109 | $unsublink = "https://" . $current_link . "/unsubscribe.php?id=" . $id; 110 | 111 | $to = $email; 112 | $subject = "A certificate for " . htmlspecialchars($domain) . " expired " . htmlspecialchars($days) . " days ago"; 113 | $message = "Hello,\r\n\r\nYou have a subscription to monitor the certificate of " . htmlspecialchars($domain) . " with the the " . $title . ". This is a service which monitors an SSL certificate on a website, and notifies you when it is about to expire. This extra notification helps you remember to renew your certificate on time.\r\n\r\nWe've noticed that the following domain has a certificate in its chain that has expired " . htmlspecialchars($days) . " days ago:\r\n\r\nDomain: " . htmlspecialchars($domain) . "\r\nCertificate Common Name: " . htmlspecialchars($cert_cn) . "\r\nCertificate Subject: " . htmlspecialchars($cert_subject) . "\r\nCertificate Serial: " . htmlspecialchars($cert_serial) . "\r\nCertificate Valid From: " . htmlspecialchars(date("Y-m-d H:i:s T", $cert_validfrom_date)) . " (" . $cert_valid_days_ago . " days ago)\r\nCertificate Valid Until: " . htmlspecialchars(date("Y-m-d H:i:s T", $cert_expiry_date)) . " (" . $cert_valid_days_ahead . " days ago)\r\n\r\nYou should renew and replace your certificate right now. If you haven't set up the certificate yourself, please forward this email to the person/company that did this for you.\r\n\rThis website is now non-functional and displays errors to its users. Please fix this issue as soon as possible.\r\n\r\nTo unsubscribe from notifications for this domain please click or copy and paste the below link in your browser:\r\n\r\n" . $unsublink . "\r\n\r\n\r\n Have a nice day,\r\nThe " . $title . " service.\r\nhttps://" . $current_link . ""; 114 | $message = wordwrap($message, 70, "\r\n"); 115 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 116 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 117 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 118 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 119 | 'X-Coffee: Black' . "\r\n" . 120 | 'List-Unsubscribe: " . "\r\n" . 121 | 'X-Mailer: PHP/4.1.1'; 122 | 123 | if (mail($to, $subject, $message, $headers) === true) { 124 | echo "\nExpired x days ago mail sent to $to.\n"; 125 | return true; 126 | } else { 127 | echo "\nCan't send expired x days ago email.\n"; 128 | return false; 129 | } 130 | } 131 | } 132 | 133 | } 134 | 135 | function send_expires_in_email($days, $domain, $email, $raw_cert) { 136 | global $current_domain; 137 | global $current_link; 138 | global $check_file; 139 | global $title; 140 | $domain = trim($domain); 141 | echo "\nDomain " . $domain . " expires in " . $days . " days.\n"; 142 | 143 | $file = file_get_contents($check_file); 144 | if ($file === FALSE) { 145 | echo "\nCan't open database.\n"; 146 | return false; 147 | } 148 | $json_a = json_decode($file, true); 149 | if ($json_a === null && json_last_error() !== JSON_ERROR_NONE) { 150 | echo "\nCan't read database.\n"; 151 | return false; 152 | } 153 | 154 | foreach ($json_a as $key => $value) { 155 | 156 | if ($value["domain"] == $domain && $value["email"] == $email) { 157 | 158 | $id = $key; 159 | $cert_cn = cert_cn($raw_cert); 160 | $cert_subject = cert_subject($raw_cert); 161 | $cert_serial = cert_serial($raw_cert); 162 | $cert_expiry_date = cert_expiry_date($raw_cert); 163 | $cert_validfrom_date = cert_valid_from($raw_cert); 164 | 165 | $now = time(); 166 | $datefromdiff = $now - $cert_validfrom_date; 167 | $datetodiff = $cert_expiry_date - $now; 168 | $cert_valid_days_ago = floor($datefromdiff/(60*60*24)); 169 | $cert_valid_days_ahead = floor($datetodiff/(60*60*24)); 170 | 171 | $unsublink = "https://" . $current_link . "/unsubscribe.php?id=" . $id; 172 | 173 | $to = $email; 174 | $subject = "A certificate for " . htmlspecialchars($domain) . " expires in " . htmlspecialchars($days) . " days"; 175 | $message = "Hello,\r\n\r\nYou have a subscription to monitor the certificate of " . htmlspecialchars($domain) . " with the the " . $title . ". This is a service which monitors an SSL certificate on a website, and notifies you when it is about to expire. This extra notification helps you remember to renew your certificate on time.\r\n\r\nWe've noticed that the following domain has a certificate in its chain that will expire in " . htmlspecialchars($days) . " days:\r\n\r\nDomain: " . htmlspecialchars($domain) . "\r\nCertificate Common Name: " . htmlspecialchars($cert_cn) . "\r\nCertificate Subject: " . htmlspecialchars($cert_subject) . "\r\nCertificate Serial: " . htmlspecialchars($cert_serial) . "\r\nCertificate Valid From: " . htmlspecialchars(date("Y-m-d H:i:s T", $cert_validfrom_date)) . " (" . $cert_valid_days_ago . " days ago)\r\nCertificate Valid Until: " . htmlspecialchars(date("Y-m-d H:i:s T", $cert_expiry_date)) . " (" . $cert_valid_days_ahead . " days left)\r\n\r\nYou should renew and replace your certificate before it expires. If you haven't set up the certificate yourself, please forward this email to the person/company that did this for you.\r\n\r\nNot replacing your certificate before the expiry date will result in a non-functional website with errors.\r\n\r\nTo unsubscribe from notifications for this domain please click or copy and paste the below link in your browser:\r\n\r\n" . $unsublink . "\r\n\r\n\r\n Have a nice day,\r\nThe " . $title . " service.\r\nhttps://" . $current_link . ""; 176 | $message = wordwrap($message, 70, "\r\n"); 177 | $headers = 'From: noreply@' . $current_domain . "\r\n" . 178 | 'Reply-To: noreply@' . $current_domain . "\r\n" . 179 | 'Return-Path: noreply@' . $current_domain . "\r\n" . 180 | 'X-Visitor-IP: ' . $visitor_ip . "\r\n" . 181 | 'X-Coffee: Black' . "\r\n" . 182 | 'List-Unsubscribe: " . "\r\n" . 183 | 'X-Mailer: PHP/4.1.1'; 184 | 185 | if (mail($to, $subject, $message, $headers) === true) { 186 | echo "\nExpires in mail sent to $to.\n"; 187 | return true; 188 | } else { 189 | echo "\nCan't send expires in email.\n"; 190 | return false; 191 | } 192 | } 193 | } 194 | } 195 | 196 | 197 | ?> -------------------------------------------------------------------------------- /js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.0",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus","focus"==b.type)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.0",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c="prev"==a?-1:1,d=this.getItemIndex(b),e=(d+c)%this.$items.length;return this.$items.eq(e)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i="next"==b?"first":"last",j=this;if(!f.length){if(!this.options.wrap)return;f=this.$element.find(".item")[i]()}if(f.hasClass("active"))return this.sliding=!1;var k=f[0],l=a.Event("slide.bs.carousel",{relatedTarget:k,direction:h});if(this.$element.trigger(l),!l.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var m=a(this.$indicators.children()[this.getItemIndex(f)]);m&&m.addClass("active")}var n=a.Event("slid.bs.carousel",{relatedTarget:k,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),j.sliding=!1,setTimeout(function(){j.$element.trigger(n)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(n)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.0",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.find("> .panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('