├── .cp-installssl.ini ├── README.md ├── cp-installssl └── wrapper ├── README.md └── cp-installssl-wrapper /.cp-installssl.ini: -------------------------------------------------------------------------------- 1 | [cpanel] 2 | host="my.cpanelserver.com" 3 | port="2083" 4 | user="mylogin" 5 | pass="mypassword" 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation 2 | 3 | **IMPORTANT**: [acme.sh](https://github.com/acmesh-official/acme.sh) now supports deploying SSL certificates to cPanel, so my method is deprecated. Please use the [official acme.sh method](https://github.com/acmesh-official/acme.sh/wiki/Simple-guide-to-add-TLS-cert-to-cpanel) instead. 4 | 5 | # What is this? 6 | 7 | cp-installssl, a PHP script to install SSL certificates with cPanel's JSON API 8 | 9 | It is heavily based on [code](https://geneticcoder.blogspot.com.es/2014/07/using-cpanels-json-api-with-php-curl-to.html) by [Rob Parham](https://plus.google.com/118182322818308613582). 10 | 11 | # Why? 12 | 13 | I created the script specifically to upload [Let's Encrypt SSL](https://letsencrypt.org/) certificates to Namecheap shared hosting, which does not support Let's Encrypt out of the box. 14 | 15 | In fact you can request SSH access your Namecheap shared hosting, install [acme.sh](https://github.com/Neilpang/acme.sh), install this repository and create a cron job to call my **bash wrapper** (see below) to renew the certificates and auto install them. 16 | 17 | Obviously you can use to automate any other SSL configuration with a certificate from other providers by just using cp-installssl PHP script. 18 | 19 | # How to use it? 20 | 21 | To get help just run: 22 | ``` 23 | ./cp-installssl 24 | ``` 25 | You will need an INI file with the configuration (host, port, user, password...) to access your CPanel. You can either place it at `~/.cp-installssl.ini` or create an environment variable `CPINSTALSSL_CFG` to point to any other file with the config. 26 | 27 | You have an example at this very same repository. 28 | 29 | If you are having 403 HTTP errros when using cp-installssl, it's because your password has non-alphanumerical characters. Please not that all values should be enclosed within double quotes (`"`) as at the example. 30 | 31 | # Bash wrapper 32 | 33 | Inside the **wrapper** folder you will find a bash script, which can be useful to combine cp-intallssl and acme.sh to autorenew and install certificates. 34 | 35 | Checkout its [README.md](wrapper/README.md) for more information, and a link to a step by step tutorial. 36 | 37 | # Restrictions 38 | 39 | The script will only work with cPanel installations supporting the JSON API v2, and using SSL. 40 | 41 | Also note that you will need cURL enabled in PHP at the instance where you plan to run this script (does not need to be the same running cPanel). 42 | 43 | # License 44 | 45 | [WTF Public License](http://www.wtfpl.net/) 46 | -------------------------------------------------------------------------------- /cp-installssl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | cpanelHost = $host; 35 | $this->cpanelPort = $port; 36 | $this->username = $user; 37 | $this->password = $pass; 38 | $this->logcurl = false; 39 | $this->cookiefile = "cpsm_cookie_".rand(99999, 9999999).".txt"; 40 | $this->logIn(); 41 | } 42 | 43 | 44 | /** 45 | * Installs a SSL certificate 46 | * @param string $domain Domaing to use with the certifcates 47 | * @param string $certfile Path to the certificate file 48 | * @param string $keyfile Path to the private key file 49 | * @param string $cafile Path to the CA certificate file 50 | * @return bool 51 | */ 52 | public function installSSL($domain, $certfile, $keyfile, $cafile = null) 53 | { 54 | $params = 'user='.$this->username.'&pass='.$this->password; 55 | ; 56 | $crt = urlencode(file_get_contents($certfile)); 57 | $key = urlencode(file_get_contents($keyfile)); 58 | $cabundle = isset($cafile) ? urlencode(file_get_contents($cafile)) : null; 59 | $url = "https://".$this->cpanelHost.":".$this->cpanelPort.$this->cpsess."/json-api/cpanel". 60 | "?cpanel_jsonapi_version=2". 61 | "&cpanel_jsonapi_func=installssl". 62 | "&cpanel_jsonapi_module=SSL&". 63 | "domain=".$domain."&". 64 | "crt=".$crt."&". 65 | "key=".$key."&". 66 | "cabundle=".$cabundle; 67 | $answer = json_decode($this->request($url, $params), true); 68 | if (isset($answer['cpanelresult']['data'][0])) { 69 | $text = $answer['cpanelresult']['data'][0]['output']; 70 | $retcode = ($answer['cpanelresult']['data'][0]['result'] == 1) ? true : false; 71 | } else { 72 | $text = $answer['cpanelresult']['data']['reason']; 73 | $retcode = ($answer['cpanelresult']['data']['result'] == 1) ? true : false; 74 | } 75 | return array($text, $retcode); 76 | } 77 | 78 | 79 | /** 80 | * Turns cURL logging on 81 | * @param int $curlfile Path to curl log file 82 | * @return array 83 | */ 84 | public function logCurl($curlfile = "cpmm_curl_log.txt") 85 | { 86 | if (!file_exists($curlfile)) { 87 | try { 88 | fopen($curlfile, "w"); 89 | } catch (Exception $ex) { 90 | if (!file_exists($curlfile)) { 91 | return $ex.'Cookie file missing.'; 92 | exit; 93 | } 94 | return true; 95 | } 96 | } elseif (!is_writable($curlfile)) { 97 | return 'Cookie file not writable.'; 98 | exit; 99 | } 100 | $this->logcurl = true; 101 | return true; 102 | } 103 | 104 | /** 105 | * Starts a session on the cPanel server 106 | * @access private 107 | */ 108 | private function logIn() 109 | { 110 | $url = 'https://'.$this->cpanelHost.":".$this->cpanelPort."/login/?login_only=1"; 111 | $url .= "&user=".$this->username."&pass=".urlencode($this->password); 112 | $answer = $this->request($url); 113 | $answer = json_decode($answer, true); 114 | if (isset($answer['status']) && $answer['status'] == 1) { 115 | $this->cpsess = $answer['security_token']; 116 | $this->homepage = 'https://'.$this->cpanelHost.":".$this->cpanelPort.$answer['redirect']; 117 | } 118 | } 119 | 120 | /** 121 | * Makes an HTTP request 122 | * @access private 123 | */ 124 | private function request($url, $params = array()) 125 | { 126 | if ($this->logcurl) { 127 | $curl_log = fopen($this->curlfile, 'a+'); 128 | } 129 | if (!file_exists($this->cookiefile)) { 130 | @fopen($this->cookiefile, "w"); 131 | if (!file_exists($this->cookiefile)) { 132 | echo 'Cookie file missing. '.$this->cookiefile; 133 | exit; 134 | } 135 | } elseif (!is_writable($this->cookiefile)) { 136 | echo 'Cookie file not writable. '.$this->cookiefile; 137 | exit; 138 | } 139 | $ch = curl_init(); 140 | $curlOpts = array( 141 | CURLOPT_URL => $url, 142 | CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 6.3; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0', 143 | CURLOPT_SSL_VERIFYPEER => false, 144 | CURLOPT_RETURNTRANSFER => true, 145 | CURLOPT_COOKIEJAR => realpath($this->cookiefile), 146 | CURLOPT_COOKIEFILE => realpath($this->cookiefile), 147 | CURLOPT_FOLLOWLOCATION => true, 148 | CURLOPT_HTTPHEADER => array( 149 | "Host: ".$this->cpanelHost, 150 | "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 151 | "Accept-Language: en-US,en;q=0.5", 152 | "Accept-Encoding: gzip, deflate", 153 | "Connection: keep-alive", 154 | "Content-Type: application/x-www-form-urlencoded") 155 | ); 156 | if (!empty($params)) { 157 | $curlOpts[CURLOPT_POST] = true; 158 | $curlOpts[CURLOPT_POSTFIELDS] = $params; 159 | } 160 | if ($this->logcurl) { 161 | $curlOpts[CURLOPT_STDERR] = $curl_log; 162 | $curlOpts[CURLOPT_FAILONERROR] = false; 163 | $curlOpts[CURLOPT_VERBOSE] = true; 164 | } 165 | curl_setopt_array($ch, $curlOpts); 166 | $answer = curl_exec($ch); 167 | if (curl_error($ch)) { 168 | echo curl_error($ch); 169 | exit; 170 | } 171 | curl_close($ch); 172 | if ($this->logcurl) { 173 | fclose($curl_log); 174 | } 175 | return (@gzdecode($answer)) ? gzdecode($answer) : $answer; 176 | } 177 | } 178 | 179 | /** 180 | * Print a message showing that the given filename does not exist 181 | * @param string $filename The path for the filename 182 | * @return array 183 | */ 184 | function print_file_not_exists($filename) 185 | { 186 | printf("File %s does not exist\n", $filename); 187 | } 188 | 189 | $cfgenvvar = 'CPINSTALSSL_CFG'; 190 | $cfgdefaultfile = '/.cp-installssl.ini'; 191 | $scriptname = basename($argv[0]); 192 | 193 | // Validate number of parameters, and show help if incorrect 194 | if ($argc < 4) { 195 | printf("%s - Script to install SSL certificates with cPanel's JSON API\n", $scriptname); 196 | printf("\n"); 197 | printf("Use: %s [cafile]\n", $scriptname); 198 | printf("\n"); 199 | printf("You need to create a config file before using the script, either at\n"); 200 | printf("~%s or at any other place (use the environment variable\n", $cfgdefaultfile); 201 | printf("%s)\n", $cfgenvvar); 202 | printf("\n"); 203 | printf("Format for the config file is:\n"); 204 | printf("\n"); 205 | printf(" [cpanel]\n"); 206 | printf(" host=\"my.cpanelserver.com\"\n"); 207 | printf(" port=\"2083\"\n"); 208 | printf(" user=\"mylogin\"\n"); 209 | printf(" pass=\"mypassword\"\n"); 210 | exit; 211 | } 212 | 213 | // Save options 214 | $options['domain'] = $argv[1]; 215 | $options['certfile'] = $argv[2]; 216 | $options['keyfile'] = $argv[3]; 217 | $options['cafile'] = isset($argv[4]) ? $argv[4] : null; 218 | 219 | // Validate if all files exist 220 | switch (false) { 221 | case file_exists($options['certfile']): 222 | print_file_not_exists($options['certfile']); 223 | exit; 224 | case file_exists($options['keyfile']): 225 | print_file_not_exists($options['keyfile']); 226 | exit; 227 | case file_exists($options['cafile']): 228 | if (isset($options['cafile'])) { 229 | print_file_not_exists($options['cafile']); 230 | exit; 231 | } 232 | } 233 | 234 | // Load config, either with the path at the environment variable CPINSTALSSL_CFG 235 | // or at ~/.cp-installssl.ini 236 | $configfile = (!getenv($cfgenvvar)) ? getenv("HOME").$cfgdefaultfile : getenv($cfgenvvar); 237 | if (file_exists($configfile)) { 238 | $config = parse_ini_file($configfile); 239 | } else { 240 | print_file_not_exists($configfile); 241 | exit; 242 | } 243 | 244 | printf("====================================================================\n"); 245 | printf(" CONFIG_FILE => %s\n", $configfile); 246 | printf(" HOST:PORT => %s:%s\n", $config['host'], $config['port']); 247 | printf("\n"); 248 | printf(" DOMAIN => %s\n", $options['domain']); 249 | printf(" CERTIFICATE => %s\n", $options['certfile']); 250 | printf(" PRIVATE_KEY => %s\n", $options['keyfile']); 251 | printf(" CABUNDLE => %s\n", $options['cafile']); 252 | printf("====================================================================\n"); 253 | 254 | // Install the certificate 255 | $cpsm = new cPanelSSLManager($config['user'], $config['pass'], $config['host'], $config['port']); 256 | $result = $cpsm->installSSL($options['domain'], $options['certfile'], $options['keyfile'], $options['cafile']); 257 | 258 | // Remove cookie file 259 | unlink($cpsm->cookiefile); 260 | 261 | // Print result message and exit 262 | printf("%s\n", $result[0]); 263 | exit($result[1] ? 0 : 1); 264 | 265 | ?> 266 | -------------------------------------------------------------------------------- /wrapper/README.md: -------------------------------------------------------------------------------- 1 | # What is this? 2 | 3 | cp-installssl-wrapper, a bash script to automate Let's Encrypt SSL certificate renewals and installation to with cPanel's JSON API 4 | 5 | It depends on [acme.sh](https://github.com/Neilpang/acme.sh) to create/renew the certificates and [cp-installssl](https://github.com/juliogonzalez/cp-installssl) to install them. 6 | 7 | Ideally you should call this script from Cron so you don't ever need to take care of the renewals. 8 | 9 | # How to use it? 10 | 11 | 1. Install acme.sh and issue the certificates(s) for the first time. 12 | 2. Clone this repository. 13 | 3. Follow [instructions](../README.md) to configure cp-installssl. 14 | 4. Learn how to run this wrapper with: 15 | ``` 16 | ./cp-installssl-wrapper -h 17 | ``` 18 | 5. Use the wrapper to force a certificate renewall and install it to cPanel. 19 | 6. Create a crontab job to call the wrapper and take care of further renewals and installations. 20 | 21 | You can get more details about the steps above by following the [step by step tutorial](https://www.juliogonzalez.es/lets-encrypt-ssl-certificates-at-cpanel-without-native-support-for-example-at-namecheap) I created to explain how I configured my Namecheap account. 22 | 23 | # License 24 | 25 | [WTF Public License](http://www.wtfpl.net/) 26 | -------------------------------------------------------------------------------- /wrapper/cp-installssl-wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BASE_DIR=$(dirname "${0}") 3 | SCRIPT=$(basename ${0}) 4 | 5 | help() { 6 | echo "" 7 | echo "Script to renew Let's encrypt SSL certificates and install them to CPanel" 8 | echo "(it requires acme.sh and cp-installssl)" 9 | echo "" 10 | echo "Syntax: " 11 | echo "" 12 | echo "${SCRIPT} -d '' -a -c [-r ] [-f] [-s]" 13 | echo "" 14 | echo "Where: " 15 | echo "" 16 | echo " -d '' Mandatory: is a whitespace separated list" 17 | echo " of second level domains with (optional) third level" 18 | echo " domains for which we want certificates." 19 | echo " For example:" 20 | echo " - mydomain.com will renew and install certificate for" 21 | echo " mydomain.com" 22 | echo " - [@.,www.]mydomain.com will renew a single certificate" 23 | echo " for mydomain.com and www.mydomain.com, but will install" 24 | echo " the certificate two times for the different hostnames." 25 | echo "" 26 | echo " -a Mandatory: Specify a filesystem path to a" 27 | echo " https://github.com/Neilpang/acme.sh git clone" 28 | echo "" 29 | echo " -c Mandatory: Specify a filesystem path to a" 30 | echo " https://github.com/juliogonzalez/cp-installssl git clone" 31 | echo "" 32 | echo " -r Optional: Specify a filesystem path to the directory where" 33 | echo " acme.sh data (private keys, certificates, etc) are stored." 34 | echo " If the parameter is not specofied then ~/.acme.sh/ will be" 35 | echo " used by default" 36 | echo "" 37 | echo " -f Optional: Will force the certificates renewal" 38 | echo "" 39 | echo " -s Optional: Will use Let's Encrypt staging/test servers" 40 | echo "" 41 | } 42 | 43 | print_invalid_syntax() { 44 | echo "Invalid syntax. For help use: ${SCRIPT} -h" 45 | } 46 | 47 | while getopts "d:a:c:r::fsh" opts; do 48 | case "${opts}" in 49 | d) HOSTS="${OPTARG}";; 50 | a) ACMEDIR="${OPTARG%/}";; 51 | c) CPINSTALLSSLDIR="${OPTARG%/}";; 52 | r) ACMEDATADIR="${OPTARG%/}";; 53 | f) FORCE='--force';; 54 | s) STAGING='--staging';; 55 | h) help 56 | exit 0;; 57 | *) print_invalid_syntax 58 | exit 1;; 59 | esac 60 | done 61 | shift $((OPTIND-1)) 62 | 63 | if [ -z "${HOSTS}" -o -z "${ACMEDIR}" -o -z "${CPINSTALLSSLDIR}" ]; then 64 | print_invalid_syntax 65 | exit 1 66 | fi 67 | 68 | if [ -z "${ACMEDATADIR}" ]; then 69 | ACMEDATADIR="${HOME}/.acme.sh" 70 | fi 71 | 72 | for HOST in ${HOSTS}; do 73 | PARENTHOST="$(echo ${HOST}|cut -d']' -f2)" 74 | SUBDOMAINS="$(echo ${HOST}|cut -d']' -f1|tr -d '['|tr ',' ' ')" 75 | if [ "${SUBDOMAINS}" == "${PARENTHOST}" ]; then SUBDOMAINS='@.'; fi 76 | echo "####################################################################" 77 | echo " ${PARENTHOST} (subdomains: ${SUBDOMAINS})" 78 | echo "####################################################################" 79 | LOG="$(${ACMEDIR}/acme.sh --home ${ACMEDATADIR} --renew -d ${PARENTHOST} ${FORCE} ${STAGING})" 80 | if [ $? -eq 0 ]; then 81 | if [ "$(echo -e ${LOG}|tail -n3|head -n1|grep 'Skip, Next renewal time is')" == "" ]; then 82 | echo "[$(date)] Certificate renewed. Installing..." 83 | CER="${ACMEDATADIR}/${PARENTHOST}/${PARENTHOST}.cer" 84 | KEY="${ACMEDATADIR}/${PARENTHOST}/${PARENTHOST}.key" 85 | for SUBDOMAIN in ${SUBDOMAINS}; do 86 | if [ "${SUBDOMAIN}" == "@." ]; then SUBDOMAIN=""; fi 87 | ${CPINSTALLSSLDIR}/cp-installssl ${SUBDOMAIN}${PARENTHOST} ${CER} ${KEY} 88 | done 89 | fi 90 | else 91 | echo -e "${LOG}" 92 | if [ "$(echo -e ${LOG}|tail -n3|head -n1|grep 'Skip, Next renewal time is')" == "" ]; then 93 | exit 1 94 | fi 95 | fi 96 | done 97 | --------------------------------------------------------------------------------