├── requirements.txt
├── .gitignore
├── .github
└── FUNDING.yml
├── .gitattributes
├── test
├── inst.sh
├── watcher.sh
├── gencsr.conf
├── dom.key
├── account.key
├── gencsr
├── check.sh
├── travis_check.sh
└── lampi
├── renewcron.sh
├── .travis.yml
├── LICENSE
├── config.json
├── acme_tiny.py
├── letsacme.py
└── README.md
/requirements.txt:
--------------------------------------------------------------------------------
1 | argparse
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ngrok
2 | *.zip
3 | dom.list
4 | dom.csr
5 | dom.crt
6 | /test/config.json
7 | chain.crt
8 | ngrok.yml
9 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: neurobin
2 | liberapay: neurobin
3 | ko_fi: neurobin
4 | issuehunt: neurobin
5 | open_collective: neurobin
6 | otechie: neurobin
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.md linguist-documentation
2 | *.sh linguist-vendored
3 | /test/* linguist-vendored
4 | *.txt linguist-documentation
5 | *.csr linguist-vendored
6 | *.list linguist-vendored
7 | *.json linguist-vendored
8 | *.crt linguist-vendored
9 | *.log linguist-vendored
10 | *.key linguist-vendored
11 |
--------------------------------------------------------------------------------
/test/inst.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$BASH_SOURCE")"
3 |
4 | #sudo add-apt-repository -y ppa:ondrej/apache2
5 | #sudo add-apt-repository -y ppa:ondrej/php5
6 | sudo apt-get update
7 | sudo apt-get install -qq apache2
8 | sudo apt-get install -qq jq
9 |
10 | #download ngrok
11 | wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -O ngrok-stable-linux-amd64.zip
12 | unzip ngrok-stable-linux-amd64.zip
13 |
--------------------------------------------------------------------------------
/test/watcher.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | pn=travis_check.sh
3 | ids=$(pidof "$pn" -x)
4 | t1=$(date +%s)
5 | threshold=$(expr 60 \* 1)
6 |
7 | while true;do
8 | t2=$(date +%s)
9 | time=$(expr $t2 - $t1)
10 | if [ $time -ge $threshold ]; then
11 | t1=$(date +%s)
12 | ids=$(pidof "$pn" -x)
13 | if [ "x$ids" != 'x' ]; then
14 | kill -s SIGINT $ids
15 | ./test/$pn
16 | fi
17 | break
18 | fi
19 | done
20 |
--------------------------------------------------------------------------------
/test/gencsr.conf:
--------------------------------------------------------------------------------
1 | ############# gencsr config file #####################
2 | # Do not use quotation marks (', "")
3 | # To prevent any entry being included, comment them
4 | # by adding a # at the beginning
5 | ######################################################
6 | CountryCode=US # Put two character country code
7 | State=My state # Put state name
8 | Locality=My city # Put city name
9 | Oraganization=My organization # Put organization name
10 | OraganizationUnit=Technology or whatever # Put organization unit name
11 | Email=mymail@somedomain.com # Put email address
12 |
--------------------------------------------------------------------------------
/renewcron.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | while true;do
3 | if python /path/to/letsacme.py --account-key /path/to/account.key \
4 | --csr /path/to/domain.csr \
5 | --config-json /path/to/config1.json \
6 | --cert-file /path/to/signed.crt \
7 | --chain-file /path/to/chain.crt \
8 | > /path/to/fullchain.crt \
9 | 2>> /path/to/letsacme.log
10 | then
11 | # echo "Successfully renewed certificate"
12 | service apache2 restart
13 | break
14 | else
15 | sleep `tr -cd 0-9 &1)"
21 | - pv1="Python 2.6"
22 | - pv2="Python 3.2"
23 | - pylint letsacme.py ;if [[ ! $? -eq 1 ]]; then echo 'pylint success!';elif [[ "$pv" = "$pv1"* || "$pv" = "$pv2"* ]]; then echo "pylint is broken in 2.6 and 3.2 does not recognize u'', pylint error for 2.6 and 3.2 should be ignored";else false; fi
24 | - ./test/travis_check.sh
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Daniel Roesler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "example.org": {
3 | "DocumentRoot":"/var/www/public_html",
4 | "_comment":"Global defintion AcmeDir won't be used as DocumentRoot is defined"
5 | },
6 | "subdomain1.example.org": {
7 | "DocumentRoot":"/var/www/subdomain1",
8 | "_comment":"Local defintion of DocumentRoot"
9 | },
10 | "www.subdomain2.example.org": {
11 | "AcmeDir":"/var/www/subdomain2",
12 | "_comment":"Local defintion of AcmeDir"
13 | },
14 | "subdomain3.example.org": {
15 | "AcmeDir":"/var/www/subdomain3"
16 | },
17 | "subdomain4.example.org": {
18 | "AcmeDir":"/var/www/subdomain4"
19 | },
20 | "www.subdomain5.example.org": {
21 | "AcmeDir":"/var/www/subdomain5"
22 | },
23 | "AccountKey":"account.key",
24 | "CSR": "domain.csr",
25 | "AcmeDir":"/var/www/html",
26 | "DocumentRoot":"/var/www/public_html",
27 | "_comment":"Global definition of DocumentRoot and AcmeDir",
28 | "CertFile":"domain.crt",
29 | "ChainFile":"chain.crt",
30 | "CA":"",
31 | "__comment":"For CA default value will be used. please don't change this option. Use the Test property if you want the staging api.",
32 | "NoChain":"False",
33 | "NoCert":"False",
34 | "Test":"False",
35 | "Force":"False",
36 | "Quiet":"False"
37 | }
38 |
--------------------------------------------------------------------------------
/test/dom.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | 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
51 | -----END RSA PRIVATE KEY-----
52 |
--------------------------------------------------------------------------------
/test/account.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | 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
51 | -----END RSA PRIVATE KEY-----
52 |
--------------------------------------------------------------------------------
/test/gencsr:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | ver='0.0.2'
5 | auth='Md. Jahidul Hamid'
6 | bug='https://github.com/neurobin/gencsr/issues'
7 | ver_info="
8 | ######### gencsr (CSR generator) #############
9 | Version: $ver
10 | Author: $auth
11 | Bug Tracker: $bug
12 | "
13 |
14 | help="
15 | ######### gencsr (CSR generator) #############
16 | Usage:
17 | gencsr [options]
18 | Options:
19 | -df | --dom-file : file containing domain per line
20 | -k | --key : private key file
21 | -ks | --key-size : key size
22 | -csr | --csr : CSR file
23 | -c | --conf : configuration file
24 | -n | --new : Always create new
25 | -h | --help : show help
26 | -v | --version : show version info
27 | "
28 |
29 |
30 | domfile=dom.list # Path to file containing domain names per line
31 | dkeyfile=dom.key # Path to a private key file (to be created)
32 | csrfile=dom.csr # Path to the CSR file (to be created)
33 | conf=gencsr.conf # Configuration file
34 | key_size=4096 # key size
35 | new=false # if true, gencsr will always create new files without using existing ones
36 |
37 |
38 | err(){
39 | echo "E: $@" >>/dev/stderr
40 | }
41 |
42 | get_arg(){
43 | if [ -z "$1" ]; then
44 | err 'Arg missing!'
45 | exit 1
46 | else
47 | echo "$1"
48 | fi
49 | }
50 |
51 | while [ $# -gt 0 ]; do
52 | case "$1" in
53 | -df|--dom-file)
54 | domfile="$(get_arg "$2")"
55 | shift
56 | ;;
57 | -k|--key)
58 | dkeyfile="$(get_arg "$2")"
59 | shift
60 | ;;
61 |
62 | -csr|--csr)
63 | csrfile="$(get_arg "$2")"
64 | shift
65 | ;;
66 | -c|--conf)
67 | conf="$(get_arg "$2")"
68 | shift
69 | ;;
70 | -ks|--key-size)
71 | key_size="$(get_arg "$2")"
72 | if [[ ! "$key_size" =~ ^[0-9]+$ ]]; then
73 | err "Invalid key size: $key_size"
74 | exit 1
75 | fi
76 | shift
77 | ;;
78 | -n|--new)
79 | new=true
80 | ;;
81 | -h|--help)
82 | echo "$help"
83 | exit 0
84 | ;;
85 | -v|--version)
86 | echo "$ver_info"
87 | exit 0
88 | ;;
89 | *)
90 | err "Invalid arg: $1"
91 | exit 1
92 | ;;
93 | esac
94 | shift
95 | done
96 |
97 | if [ ! -f "$dkeyfile" ] || $new; then
98 | echo 'Creating new key file: '"$dkeyfile"
99 | if openssl genrsa $key_size > "$dkeyfile"; then
100 | echo 'Successfully created key file: '"$dkeyfile"
101 | else
102 | err "Failed to created key file: $dkeyfile"
103 | exit 1
104 | fi
105 | else
106 | echo "Using existing key file: $dkeyfile"
107 | fi
108 |
109 | CN="$(head -1 "$domfile")"
110 | subjectAltName="$(sed -e '/^[[:blank:]]*$/d' \
111 | -e 's/^[[:blank:]]*//' \
112 | -e 's/[[:blank:]]*$//' \
113 | -e 's/^/DNS:/' "$domfile" |
114 | tr '\n' ',')"
115 | subjectAltName="${subjectAltName/%,/}"
116 |
117 | country_code="$(sed -n -e 's/^[[:blank:]]*CountryCode[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
118 | state="$(sed -n -e 's/^[[:blank:]]*State[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
119 | locality="$(sed -n -e 's/^[[:blank:]]*Locality[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
120 | org="$(sed -n -e 's/^[[:blank:]]*Oraganization[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
121 | org_unit="$(sed -n -e 's/^[[:blank:]]*OraganizationUnit[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
122 | email="$(sed -n -e 's/^[[:blank:]]*Email[[:blank:]]*=[[:blank:]]*\([^#]*\).*$/\1/pi' "$conf" |sed -e 's/[[:blank:]]*$//')"
123 |
124 | subj="/CN=$CN"
125 |
126 | if [ -n "$country_code" ]; then
127 | subj="$subj/C=$country_code"
128 | fi
129 |
130 | if [ -n "$state" ]; then
131 | subj="$subj/ST=$state"
132 | fi
133 |
134 | if [ -n "$locality" ]; then
135 | subj="$subj/L=$locality"
136 | fi
137 |
138 | if [ -n "$org" ]; then
139 | subj="$subj/O=$org"
140 | fi
141 |
142 | if [ -n "$org_unit" ]; then
143 | subj="$subj/OU=$org_unit"
144 | fi
145 |
146 | if [ -n "$email" ]; then
147 | subj="$subj/emailAddress=$email"
148 | fi
149 |
150 | if openssl req -new -sha256 -key "$dkeyfile" -subj "$subj" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=$subjectAltName")) -out "$csrfile"; then
151 | echo "Successfully created CSR file: $csrfile"
152 | else
153 | err 'Failed to create CSR file!'
154 | fi
155 |
--------------------------------------------------------------------------------
/test/check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$BASH_SOURCE")"
3 |
4 |
5 | ###Some convenience functions
6 |
7 | prnt(){
8 | printf "$*\n" >>/dev/stdout
9 | }
10 | err() {
11 | printf "$*\n" >>/dev/stderr
12 | }
13 |
14 | Exit(){
15 | prnt '\nCleaning'
16 | if [ "$2" != '-p' ]; then
17 | kill $pid >/dev/null 2>&1 && prnt "\tKilled pid: $pid"
18 | fi
19 | sudo ./lampi -rmd "$site1" >/dev/null 2>&1 && prnt "\tRemoved site: $site1"
20 | sudo ./lampi -rmd "$site2" >/dev/null 2>&1 && prnt "\tRemoved site: $site2"
21 | exit $1
22 | }
23 |
24 | trap 'Exit 1 2>/dev/null' SIGINT
25 | #trap Exit INT TERM EXIT
26 |
27 |
28 |
29 | doc_root1="$(mktemp -d)"
30 | doc_root2="$(mktemp -d)"
31 | site1=letsacme-host1.local
32 | site2=letsacme-host2.local
33 | acme_dir=$doc_root1/acme-challenge
34 |
35 |
36 | prnt "\nCreating test sites..."
37 | sudo ./lampi -n "$site1" -dr "$doc_root1" >/dev/null && prnt "\tCreated site: $site1"
38 | sudo ./lampi -n "$site2" -dr "$doc_root2" >/dev/null && prnt "\tCreated site: $site2"
39 |
40 | #Test the sites
41 | prnt '\tTesting sites..'
42 | mkdir -p "$doc_root1"/.well-known/acme-challenge
43 | mkdir -p "$doc_root2"/.well-known/acme-challenge
44 |
45 | site_test(){
46 | # $1: burl, $2:docroot
47 | somefile="$(tr -cd 0-9 "$2/.well-known/acme-challenge/$somefile"
49 | if curl "$1/.well-known/acme-challenge/$somefile" >/dev/null 2>&1;then
50 | prnt "\t\tPassed: $1"
51 | else
52 | prnt "\t\tFailed: $1"
53 | Exit 1
54 | fi
55 | }
56 |
57 | site_test "http://$site1" "$doc_root1"
58 | site_test "http://$site2" "$doc_root2"
59 |
60 |
61 | prnt "\nngrok ..."
62 |
63 | nweb=127.0.0.1:4040
64 | nconf="tunnels:
65 | $site1:
66 | proto: http
67 | host_header: rewrite
68 | addr: $site1:80
69 | web_addr: $nweb
70 | $site2:
71 | proto: http
72 | host_header: rewrite
73 | addr: $site2:80
74 | web_addr: $nweb"
75 | nconf_f=ngrok.yml
76 | echo "$nconf" > "$nconf_f" && prnt '\tCreated ngrok config file'
77 |
78 | nohup ./ngrok start -config "$nconf_f" $site1 $site2 >/dev/null 2>&1 &
79 | pid=$!
80 | prnt "\tRunning ngrok in the background (pid: $pid)"
81 |
82 | t1=$(date +%s)
83 | max_t=7 #limit max try in seconds
84 | while true; do
85 | tunnel_info_json="$(curl -s http://$nweb/api/tunnels)"
86 | #echo $tunnel_info_json
87 | public_url1="$(echo "$tunnel_info_json" | jq -r '.tunnels[0].public_url' | grep 'http://')"
88 | dom1="$(echo "$public_url1" |sed -n -e 's#.*//\([^/]*\)/*.*#\1#p')"
89 | public_url2="$(echo "$tunnel_info_json" | jq -r '.tunnels[2].public_url' | grep 'http://')"
90 | dom2="$(echo "$public_url2" |sed -n -e 's#.*//\([^/]*\)/*.*#\1#p')"
91 | if [ -n "$dom1" ] && [ -n "$dom2" ]; then
92 | break
93 | fi
94 | t2=$(date +%s)
95 | time="$(expr $t2 - $t1)"
96 | if [ $time -ge $max_t ]; then
97 | t1=$(date +%s)
98 | prnt "\tngork froze. Restarting ..."
99 | kill $pid >/dev/null 2>&1 && prnt "\tKilled pid: $pid"
100 | nohup ./ngrok start -config "$nconf_f" $site1 $site2 >/dev/null 2>&1 &
101 | pid=$!
102 | prnt "\tngrok restarted (pid: $pid)"
103 | fi
104 | done
105 |
106 | if [ "$dom1" = "$dom2" ]; then
107 | err '\tE: Both domain can not be same. abort'
108 | Exit 1 2>/dev/null
109 | fi
110 |
111 | prnt "\tSite1: $public_url1"
112 | prnt "\tSite2: $public_url2"
113 | prnt "\tTesting sites ..."
114 |
115 | #tphp=''
116 |
117 | #ls -la "$doc_root1" "$doc_root2"
118 |
119 | site_test "$public_url1" "$doc_root1"
120 | site_test "$public_url2" "$doc_root2"
121 |
122 | #ls -la "$doc_root1" "$doc_root2"
123 |
124 | #sleep 30
125 |
126 | prnt '\nPreparing ...'
127 | if [ -f account.key ]; then
128 | prnt '\tUsing existing account.key'
129 | else
130 | openssl genrsa 4096 > account.key && prnt '\tCreated account.key'
131 | fi
132 | printf "$dom1\n$dom2\n" > dom.list && prnt '\tCreated dom.list file'
133 | ./gencsr >/dev/null && prnt '\tCreated CSR'
134 |
135 | prnt '
136 | *********************************************************
137 | *** Test 1: With --config-json and using document root
138 | *********************************************************
139 | '
140 |
141 | conf_json='{
142 | "'$dom1'":
143 | {
144 | "DocumentRoot": "'$doc_root1'"
145 | },
146 | "'$dom2'":
147 | {
148 | "DocumentRoot": "'$doc_root2'"
149 | },
150 | "DocumentRoot": "'$doc_root2'",
151 | "AccountKey":"account.key",
152 | "CSR": "dom.csr",
153 | "CertFile":"dom.crt",
154 | "ChainFile":"chain.crt",
155 | "Test":"True"
156 | }' && prnt '\tConfiguration JSON prepared'
157 | #echo "$conf_json" > config.json && prnt '\tCreated config.json file'
158 |
159 | prnt '\tRunning letsacme: python ../letsacme.py --config-json $conf_json'
160 |
161 | t="$(mktemp)"
162 | { python ../letsacme.py --config-json "$conf_json" 2>&1; echo $? >"$t"; } | sed -e 's/.*/\t\t&/'
163 | es=$(cat $t)
164 | rm "$t"
165 | if [ $es -eq 0 ]; then
166 | prnt '\n\t*** success on test 1 ***'
167 | else
168 | err '\tE: Failed to get the certs'
169 | #sleep 30
170 | Exit 1 2>/dev/null
171 | fi
172 |
173 | prnt "
174 | *******************************************************
175 | *** Test 2: With --acme-dir and without --config-json
176 | *******************************************************
177 | "
178 |
179 | red_code="
180 | RewriteEngine On
181 | RewriteBase /
182 | RewriteRule ^.well-known/acme-challenge/(.*)$ http://$dom1/acme-challenge/\$1 [L,R=302]
183 | "
184 | echo "$red_code" > "$doc_root1"/.htaccess && prnt "\tRedirect for $site1 is set"
185 | echo "$red_code" > "$doc_root2"/.htaccess && prnt "\tRedirect for $site2 is set"
186 |
187 | prnt "\tRunning letsacme:
188 | \tpython ../letsacme.py --test\\\\\n\t --account-key account.key\\\\\n\t --csr dom.csr\\\\\n\t --acme-dir $acme_dir\\\\\n\t --chain-file chain.crt\\\\\n\t --cert-file dom.crt"
189 | t="$(mktemp)"
190 | { python ../letsacme.py --test --account-key account.key --csr dom.csr --acme-dir "$acme_dir" --chain-file chain.crt --cert-file dom.crt 2>&1; echo $? >"$t"; } | sed -e 's/.*/\t\t&/'
191 | es=$(cat "$t")
192 | rm $t
193 | if [ $es -eq 0 ]; then
194 | prnt '\n\t*** success on test 2 ***'
195 | else
196 | err '\tE: Failed to get the certs'
197 | Exit 1 2>/dev/null
198 | fi
199 |
200 | ############## Final cleaning ###############
201 | Exit 0 2>/dev/null
202 | #############################################
203 |
--------------------------------------------------------------------------------
/test/travis_check.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | cd "$(dirname "$BASH_SOURCE")"
3 |
4 | prnt(){
5 | printf "$*\n" >>/dev/stdout
6 | }
7 | err() {
8 | printf "$*\n" >>/dev/stderr
9 | }
10 |
11 | Exit(){
12 | prnt '\nCleaning'
13 | if [ "$2" != '-p' ]; then
14 | kill $pid >/dev/null 2>&1 && prnt "\tKilled pid: $pid"
15 | fi
16 | # mv -f "$doc_root1"/.htaccess.bak "$doc_root1"/.htaccess >/dev/null 2>&1
17 | # mv -f "$doc_root2"/.htaccess.bak "$doc_root2"/.htaccess >/dev/null 2>&1
18 | # sudo ./lampi -rm "$site1" >/dev/null 2>&1 && prnt "\tRemoved site: $site1"
19 | # sudo ./lampi -rm "$site2" >/dev/null 2>&1 && prnt "\tRemoved site: $site2"
20 | exit $1
21 | }
22 |
23 | trap 'Exit 1 2>/dev/null' SIGINT
24 |
25 | doc_root1=/var/www/html
26 | doc_root2=/var/www/html
27 | site1=letsacme-host1.local
28 | site2=letsacme-host2.local
29 | acme_dir=/var/www/acme-challenge
30 |
31 |
32 | prnt "\nCreating test sites..."
33 | #sudo ./lampi -n "$site1" -dr "$doc_root1" -nsr >/dev/null && prnt "\tCreated site: $site1"
34 | #sudo ./lampi -n "$site2" -dr "$doc_root2" -nsr >/dev/null && prnt "\tCreated site: $site2"
35 | #sudo sed -i .bak -e 's/ServerName.*//' "/etc/apache2/sites-available/$site1.conf"
36 | #sudo sed -i .bak -e 's/ServerName.*//' "/etc/apache2/sites-available/$site2.conf"
37 | #a2ensite $site1
38 | #a2ensite $site2
39 | #sudo service apache2 restart >/dev/null && prnt "\tReloaded apache2"
40 |
41 |
42 | #create backup .htaccess file
43 | #mv -f "$doc_root1"/.htaccess "$doc_root1"/.htaccess.bak >/dev/null 2>&1
44 | #mv -f "$doc_root2"/.htaccess "$doc_root2"/.htaccess.bak >/dev/null 2>&1
45 |
46 |
47 |
48 | sudo mkdir -p "$doc_root1"
49 | sudo mkdir -p "$doc_root2"
50 | sudo mkdir -p "$acme_dir"
51 | sudo chown -R $USER:$USER "$doc_root2" "$doc_root1" "$acme_dir"
52 |
53 | #get_conf(){
54 | # #$1: dr
55 | # prnt "
56 | ## ServerName $2
57 | ## ServerAlias $2
58 | # ServerAdmin webmaster@$2
59 | # DocumentRoot $1
60 | #
61 | # Options Indexes FollowSymLinks MultiViews
62 | # AllowOverride All
63 | # Require all granted
64 | #
65 | # "
66 | #}
67 |
68 | #get_conf "$doc_root1" "$site1" | sudo tee /etc/apache2/sites-available/"$site1".conf > /dev/null
69 | ##cat /etc/apache2/sites-available/"$site1".conf
70 |
71 | #get_conf "$doc_root2" "$site2" | sudo tee /etc/apache2/sites-available/"$site2".conf > /dev/null
72 | ##cat /etc/apache2/sites-available/"$site2".conf
73 |
74 | manage_host(){
75 | #$1:dom
76 | if ! grep -s -e "^127.0.0.1[[:blank:]]*$1[[:blank:]]*$" /etc/hosts >/dev/null 2>&1; then
77 | sudo sed -i.bak -e "1 a 127.0.0.1\t$1" /etc/hosts &&
78 | printf "\tAdded $1 to /etc/hosts\n" ||
79 | printf "\tE: Failed to add $1 to /etc/hosts\n"
80 | fi
81 | }
82 | #echo "" |sudo tee -a /etc/hosts >/dev/null
83 | #sudo sed -i.bak 's/127\.0\.0\.1.*/127.0.0.1 localhost/' /etc/hosts
84 | manage_host "$site1"
85 | manage_host "$site2"
86 | #echo " $site1 $site2" |sudo tee -a /etc/hosts
87 | #cat /etc/hosts
88 |
89 | #sudo a2dissite 000-default
90 | #sudo a2ensite "$site1" >/dev/null 2>&1 && prnt "\tCreated site: $site1"
91 | #sudo a2ensite "$site2" >/dev/null 2>&1 && prnt "\tCreated site: $site2"
92 | #sudo a2enmod rewrite >/dev/null 2>&1
93 |
94 | prnt "\nngrok ..."
95 |
96 | #download ngrok
97 | #wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -O ngrok-stable-linux-amd64.zip
98 | #unzip ngrok-stable-linux-amd64.zip
99 | nweb=127.0.0.1:4040
100 | nconf="tunnels:
101 | $site1:
102 | proto: http
103 | host_header: rewrite
104 | addr: $site1:80
105 | web_addr: $nweb
106 | $site2:
107 | proto: http
108 | host_header: rewrite
109 | addr: $site2:80
110 | web_addr: $nweb"
111 | nconf_f=ngrok.yml
112 | echo "$nconf" > "$nconf_f" && prnt '\tCreated ngrok config file'
113 |
114 | nohup ./ngrok start -config "$nconf_f" $site1 $site2 >/dev/null 2>&1 &
115 | pid=$!
116 | prnt "\tRunning ngrok in the background (pid: $pid)"
117 |
118 | t1=$(date +%s)
119 | max_t=7 #limit max try in seconds
120 | while true; do
121 | tunnel_info_json="$(curl -s http://$nweb/api/tunnels)"
122 | #echo $tunnel_info_json
123 | public_url1="$(echo "$tunnel_info_json" | jq -r '.tunnels[0].public_url' | grep 'http://')"
124 | dom1="$(echo "$public_url1" |sed -n -e 's#.*//\([^/]*\)/*.*#\1#p')"
125 | public_url2="$(echo "$tunnel_info_json" | jq -r '.tunnels[2].public_url' | grep 'http://')"
126 | dom2="$(echo "$public_url2" |sed -n -e 's#.*//\([^/]*\)/*.*#\1#p')"
127 | if [ -n "$dom1" ] && [ -n "$dom2" ]; then
128 | break
129 | fi
130 | t2=$(date +%s)
131 | time="$(expr $t2 - $t1)"
132 | if [ $time -ge $max_t ]; then
133 | t1=$(date +%s)
134 | prnt "\tngork froze. Restarting ..."
135 | kill $pid >/dev/null 2>&1 && prnt "\tKilled pid: $pid"
136 | nohup ./ngrok start -config "$nconf_f" $site1 $site2 >/dev/null 2>&1 &
137 | pid=$!
138 | prnt "\tngrok restarted (pid: $pid)"
139 | fi
140 | done
141 |
142 | if [ "$dom1" = "$dom2" ]; then
143 | err '\tE: Both domain can not be same. abort'
144 | Exit 1 2>/dev/null
145 | fi
146 |
147 | prnt "\tSite1: $public_url1"
148 | prnt "\tSite2: $public_url2"
149 |
150 |
151 | #tphp=''
152 |
153 | #echo working1 > "$doc_root1"/somefile
154 | #echo working2 > "$doc_root2"/somefile
155 |
156 | #ls -la "$doc_root1" "$doc_root2"
157 |
158 | #prnt "URL1: $public_url1/somefile"
159 | #prnt "URL2: $public_url2/somefile"
160 |
161 | #curl "$public_url1/somefile"
162 | #curl "$public_url2/somefile"
163 | #curl "$public_url1"
164 |
165 | prnt '\nPreparing ...'
166 |
167 | #openssl genrsa 4096 > account.key && prnt '\tCreated account.key'
168 | printf "$dom1\n$dom2\n" > dom.list && prnt '\tCreated dom.list file'
169 | ./gencsr >/dev/null && prnt '\tCreated CSR'
170 |
171 | prnt '
172 | *********************************************************
173 | *** Test 1: With --config-json and using document root
174 | *********************************************************
175 | '
176 |
177 | conf_json='{
178 | "'$dom1'":
179 | {
180 | "DocumentRoot": "'$doc_root1'"
181 | },
182 | "'$dom2'":
183 | {
184 | "DocumentRoot": "'$doc_root2'"
185 | },
186 | "DocumentRoot": "'$doc_root2'",
187 | "AccountKey":"account.key",
188 | "CSR": "dom.csr",
189 | "CertFile":"dom.crt",
190 | "ChainFile":"chain.crt",
191 | "CA":"",
192 | "NoChain":"False",
193 | "NoCert":"False",
194 | "Test":"True",
195 | "Force":"False"
196 | }'
197 | echo "$conf_json" > config.json && prnt '\tCreated config.json file'
198 |
199 | prnt "\tRunning letsacme: python ../letsacme.py --config-json config.json"
200 | #echo "" > "$doc_root1"/.htaccess
201 | #echo "" > "$doc_root2"/.htaccess
202 | #sleep 30
203 | t="$(mktemp)"
204 | { python ../letsacme.py --config-json config.json 2>&1; echo $? >"$t"; } | sed -e 's/.*/\t\t&/'
205 | es=$(cat $t)
206 | rm "$t"
207 | if [ $es -eq 0 ]; then
208 | prnt '\n\t*** success on test 1 ***'
209 | else
210 | err '\tE: Failed to get the certs'
211 | Exit 1 2>/dev/null
212 | fi
213 |
214 | prnt "
215 | *******************************************************
216 | *** Test 2: With --acme-dir and without --config-json
217 | *******************************************************
218 | "
219 |
220 | #red_code="
221 | #RewriteEngine On
222 | #RewriteBase /
223 | #RewriteRule ^.well-known/acme-challenge/(.*)$ http://$dom1/acme-challenge/\$1 [L,R=302]
224 | #"
225 | #echo "$red_code" > "$doc_root1"/.htaccess && prnt "\tRedirect for $site1 is set"
226 | #echo "$red_code" > "$doc_root2"/.htaccess && prnt "\tRedirect for $site2 is set"
227 |
228 | alias="Alias /.well-known/acme-challenge $acme_dir\n\nRequire all granted\n"
229 |
230 | sudo sed -i.bak -e "s#]*>#&\n$alias#" /etc/apache2/sites-available/000-default.conf
231 | #cat /etc/apache2/sites-available/000-default.conf
232 | sudo a2enmod alias >/dev/null 2>&1
233 | #sudo a2enmod actions
234 | sudo service apache2 restart >/dev/null 2>&1 && prnt '\tReloaded apache2' || err '\tE: Failed to reload apache2'
235 |
236 | prnt "\tRunning letsacme:
237 | \tpython ../letsacme.py --test\\\\\n\t --account-key account.key\\\\\n\t --csr dom.csr\\\\\n\t --acme-dir $acme_dir\\\\\n\t --chain-file chain.crt\\\\\n\t --cert-file dom.crt"
238 | t="$(mktemp)"
239 | { python ../letsacme.py --test --account-key account.key --csr dom.csr --acme-dir "$acme_dir" --chain-file chain.crt --cert-file dom.crt 2>&1; echo $? >"$t"; } | sed -e 's/.*/\t\t&/'
240 | es=$(cat "$t")
241 | rm $t
242 | if [ $es -eq 0 ]; then
243 | prnt '\n\t*** success on test 2 ***'
244 | else
245 | err '\tE: Failed to get the certs'
246 | Exit 1 2>/dev/null
247 | fi
248 |
249 | ############## Final cleaning ###############
250 | Exit 0 2>/dev/null
251 | #############################################
252 |
--------------------------------------------------------------------------------
/acme_tiny.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny
3 | import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
4 | try:
5 | from urllib.request import urlopen, Request # Python 3
6 | except ImportError:
7 | from urllib2 import urlopen, Request # Python 2
8 |
9 | DEFAULT_CA = "https://acme-v02.api.letsencrypt.org" # DEPRECATED! USE DEFAULT_DIRECTORY_URL INSTEAD
10 | DEFAULT_DIRECTORY_URL = "https://acme-v02.api.letsencrypt.org/directory"
11 |
12 | LOGGER = logging.getLogger(__name__)
13 | LOGGER.addHandler(logging.StreamHandler())
14 | LOGGER.setLevel(logging.INFO)
15 |
16 | def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None):
17 | directory, acct_headers, alg, jwk = None, None, None, None # global variables
18 |
19 | # helper functions - base64 encode for jose spec
20 | def _b64(b):
21 | return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")
22 |
23 | # helper function - run external commands
24 | def _cmd(cmd_list, stdin=None, cmd_input=None, err_msg="Command Line Error"):
25 | proc = subprocess.Popen(cmd_list, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
26 | out, err = proc.communicate(cmd_input)
27 | if proc.returncode != 0:
28 | raise IOError("{0}\n{1}".format(err_msg, err))
29 | return out
30 |
31 | # helper function - make request and automatically parse json response
32 | def _do_request(url, data=None, err_msg="Error", depth=0):
33 | try:
34 | resp = urlopen(Request(url, data=data, headers={"Content-Type": "application/jose+json", "User-Agent": "acme-tiny"}))
35 | resp_data, code, headers = resp.read().decode("utf8"), resp.getcode(), resp.headers
36 | except IOError as e:
37 | resp_data = e.read().decode("utf8") if hasattr(e, "read") else str(e)
38 | code, headers = getattr(e, "code", None), {}
39 | try:
40 | resp_data = json.loads(resp_data) # try to parse json results
41 | except ValueError:
42 | pass # ignore json parsing errors
43 | if depth < 100 and code == 400 and resp_data['type'] == "urn:ietf:params:acme:error:badNonce":
44 | raise IndexError(resp_data) # allow 100 retrys for bad nonces
45 | if code not in [200, 201, 204]:
46 | raise ValueError("{0}:\nUrl: {1}\nData: {2}\nResponse Code: {3}\nResponse: {4}".format(err_msg, url, data, code, resp_data))
47 | return resp_data, code, headers
48 |
49 | # helper function - make signed requests
50 | def _send_signed_request(url, payload, err_msg, depth=0):
51 | payload64 = "" if payload is None else _b64(json.dumps(payload).encode('utf8'))
52 | new_nonce = _do_request(directory['newNonce'])[2]['Replay-Nonce']
53 | protected = {"url": url, "alg": alg, "nonce": new_nonce}
54 | protected.update({"jwk": jwk} if acct_headers is None else {"kid": acct_headers['Location']})
55 | protected64 = _b64(json.dumps(protected).encode('utf8'))
56 | protected_input = "{0}.{1}".format(protected64, payload64).encode('utf8')
57 | out = _cmd(["openssl", "dgst", "-sha256", "-sign", account_key], stdin=subprocess.PIPE, cmd_input=protected_input, err_msg="OpenSSL Error")
58 | data = json.dumps({"protected": protected64, "payload": payload64, "signature": _b64(out)})
59 | try:
60 | return _do_request(url, data=data.encode('utf8'), err_msg=err_msg, depth=depth)
61 | except IndexError: # retry bad nonces (they raise IndexError)
62 | return _send_signed_request(url, payload, err_msg, depth=(depth + 1))
63 |
64 | # helper function - poll until complete
65 | def _poll_until_not(url, pending_statuses, err_msg):
66 | result, t0 = None, time.time()
67 | while result is None or result['status'] in pending_statuses:
68 | assert (time.time() - t0 < 3600), "Polling timeout" # 1 hour timeout
69 | time.sleep(0 if result is None else 2)
70 | result, _, _ = _send_signed_request(url, None, err_msg)
71 | return result
72 |
73 | # parse account key to get public key
74 | log.info("Parsing account key...")
75 | out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error")
76 | pub_pattern = r"modulus:[\s]+?00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)"
77 | pub_hex, pub_exp = re.search(pub_pattern, out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
78 | pub_exp = "{0:x}".format(int(pub_exp))
79 | pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
80 | alg = "RS256"
81 | jwk = {
82 | "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
83 | "kty": "RSA",
84 | "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
85 | }
86 | accountkey_json = json.dumps(jwk, sort_keys=True, separators=(',', ':'))
87 | thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
88 |
89 | # find domains
90 | log.info("Parsing CSR...")
91 | out = _cmd(["openssl", "req", "-in", csr, "-noout", "-text"], err_msg="Error loading {0}".format(csr))
92 | domains = set([])
93 | common_name = re.search(r"Subject:.*? CN\s?=\s?([^\s,;/]+)", out.decode('utf8'))
94 | if common_name is not None:
95 | domains.add(common_name.group(1))
96 | subject_alt_names = re.search(r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL)
97 | if subject_alt_names is not None:
98 | for san in subject_alt_names.group(1).split(", "):
99 | if san.startswith("DNS:"):
100 | domains.add(san[4:])
101 | log.info("Found domains: {0}".format(", ".join(domains)))
102 |
103 | # get the ACME directory of urls
104 | log.info("Getting directory...")
105 | directory_url = CA + "/directory" if CA != DEFAULT_CA else directory_url # backwards compatibility with deprecated CA kwarg
106 | directory, _, _ = _do_request(directory_url, err_msg="Error getting directory")
107 | log.info("Directory found!")
108 |
109 | # create account, update contact details (if any), and set the global key identifier
110 | log.info("Registering account...")
111 | reg_payload = {"termsOfServiceAgreed": True}
112 | account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering")
113 | log.info("Registered!" if code == 201 else "Already registered!")
114 | if contact is not None:
115 | account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details")
116 | log.info("Updated contact details:\n{0}".format("\n".join(account['contact'])))
117 |
118 | # create a new order
119 | log.info("Creating new order...")
120 | order_payload = {"identifiers": [{"type": "dns", "value": d} for d in domains]}
121 | order, _, order_headers = _send_signed_request(directory['newOrder'], order_payload, "Error creating new order")
122 | log.info("Order created!")
123 |
124 | # get the authorizations that need to be completed
125 | for auth_url in order['authorizations']:
126 | authorization, _, _ = _send_signed_request(auth_url, None, "Error getting challenges")
127 | domain = authorization['identifier']['value']
128 | log.info("Verifying {0}...".format(domain))
129 |
130 | # find the http-01 challenge and write the challenge file
131 | challenge = [c for c in authorization['challenges'] if c['type'] == "http-01"][0]
132 | token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
133 | keyauthorization = "{0}.{1}".format(token, thumbprint)
134 | wellknown_path = os.path.join(acme_dir, token)
135 | with open(wellknown_path, "w") as wellknown_file:
136 | wellknown_file.write(keyauthorization)
137 |
138 | # check that the file is in place
139 | try:
140 | wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token)
141 | assert (disable_check or _do_request(wellknown_url)[0] == keyauthorization)
142 | except (AssertionError, ValueError) as e:
143 | raise ValueError("Wrote file to {0}, but couldn't download {1}: {2}".format(wellknown_path, wellknown_url, e))
144 |
145 | # say the challenge is done
146 | _send_signed_request(challenge['url'], {}, "Error submitting challenges: {0}".format(domain))
147 | authorization = _poll_until_not(auth_url, ["pending"], "Error checking challenge status for {0}".format(domain))
148 | if authorization['status'] != "valid":
149 | raise ValueError("Challenge did not pass for {0}: {1}".format(domain, authorization))
150 | os.remove(wellknown_path)
151 | log.info("{0} verified!".format(domain))
152 |
153 | # finalize the order with the csr
154 | log.info("Signing certificate...")
155 | csr_der = _cmd(["openssl", "req", "-in", csr, "-outform", "DER"], err_msg="DER Export Error")
156 | _send_signed_request(order['finalize'], {"csr": _b64(csr_der)}, "Error finalizing order")
157 |
158 | # poll the order to monitor when it's done
159 | order = _poll_until_not(order_headers['Location'], ["pending", "processing"], "Error checking order status")
160 | if order['status'] != "valid":
161 | raise ValueError("Order failed: {0}".format(order))
162 |
163 | # download the certificate
164 | certificate_pem, _, _ = _send_signed_request(order['certificate'], None, "Certificate download failed")
165 | log.info("Certificate signed!")
166 | return certificate_pem
167 |
168 | def main(argv=None):
169 | parser = argparse.ArgumentParser(
170 | formatter_class=argparse.RawDescriptionHelpFormatter,
171 | description=textwrap.dedent("""\
172 | This script automates the process of getting a signed TLS certificate from Let's Encrypt using
173 | the ACME protocol. It will need to be run on your server and have access to your private
174 | account key, so PLEASE READ THROUGH IT! It's only ~200 lines, so it won't take long.
175 |
176 | Example Usage:
177 | python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed_chain.crt
178 |
179 | Example Crontab Renewal (once per month):
180 | 0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed_chain.crt 2>> /var/log/acme_tiny.log
181 | """)
182 | )
183 | parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key")
184 | parser.add_argument("--csr", required=True, help="path to your certificate signing request")
185 | parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory")
186 | parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
187 | parser.add_argument("--disable-check", default=False, action="store_true", help="disable checking if the challenge file is hosted correctly before telling the CA")
188 | parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt")
189 | parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!")
190 | parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:aaa@bbb.com) for your account-key")
191 |
192 | args = parser.parse_args(argv)
193 | LOGGER.setLevel(args.quiet or LOGGER.level)
194 | signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact)
195 | sys.stdout.write(signed_crt)
196 |
197 | if __name__ == "__main__": # pragma: no cover
198 | main(sys.argv[1:])
199 |
--------------------------------------------------------------------------------
/test/lampi:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | p_name=lampi
3 | p_author='Md Jahidul Hamid'
4 | p_source='https://github.com/neurobin/lampi'
5 | p_bugt="$p_source/issues"
6 | p_version=0.1.7
7 | ver_info="
8 | Name : $p_name
9 | Version : $p_version
10 | Author : $p_author
11 | Source : $p_source
12 | Bug tracker : $p_bugt
13 | "
14 | help="
15 | ############################## lampi #####################################
16 | ################### LAMP stack installer for Ubuntu ######################
17 | ##########################################################################
18 | # Usage:
19 | # lampi [-n, -dr, -i, -ri, -u, -s, -so, -rm, -rmd, -nsr, -f -nu, -v, -h]
20 | #
21 | # Run simply lampi -i to install the LAMP stack
22 | # with default configs
23 | #
24 | # Options:
25 | # -n | --name | server name (default localhost)
26 | # -dr | --doc-root | arbitrary document root
27 | # -i | --install | install LAMP
28 | # -ri | --reinstall | reinstall LAMP
29 | # -u | --uninstall | uninstall LAMP
30 | # -s | --ssl | enable SSL
31 | # -rm | --remove | remove a site
32 | # -rmd | --remove-with-dir | remove a site and it's directory
33 | # -f | --force | force
34 | # -nu | --no-update | do not run apt-get update
35 | # -so | --ssl-only | configure for https site only
36 | # -npa | --no-php-myadmin | no php my admin
37 | # -nsr | --no-server-restart | no server restart
38 | # -v | --version | show version info
39 | # -h | --help | show help
40 | ##########################################################################
41 | "
42 | dr=''
43 | ssl=false
44 | ssl_only=false
45 | no_update=false
46 | install=false
47 | localhost=localhost
48 | uninstall=false
49 | reinstall=false
50 | remove=
51 | force=false
52 | rmd=false
53 | npa=false
54 | nsr=false
55 | for op in "$@";do
56 | case "$op" in
57 | -v|--version)
58 | echo "$ver_info"
59 | shift
60 | exit 0
61 | ;;
62 | -h|--help)
63 | echo "$help"
64 | shift
65 | exit 0
66 | ;;
67 | -dr|--doc-root)
68 | shift
69 | if [[ "$1" != "" ]]; then
70 | dr="${1/%\//}"
71 | shift
72 | else
73 | echo "E: Arg missing for -dr option"
74 | exit 1
75 | fi
76 | ;;
77 | -s|--ssl)
78 | ssl=true
79 | shift
80 | ;;
81 | -so|--ssl-only)
82 | ssl=true
83 | ssl_only=true
84 | shift
85 | ;;
86 | -nu|--no-update)
87 | no_update=true
88 | shift
89 | ;;
90 | -npa|--no-php-myadmin)
91 | npa=true
92 | shift
93 | ;;
94 | -nsr|--no-server-restart)
95 | nsr=true
96 | shift
97 | ;;
98 | -i|--install)
99 | install=true
100 | reinstall=false
101 | uninstall=false
102 | shift
103 | ;;
104 | -ri|--reinstall)
105 | install=true
106 | reinstall=true
107 | uninstall=false
108 | shift
109 | ;;
110 | -u|--uninstall)
111 | install=false
112 | reinstall=false
113 | uninstall=true
114 | shift
115 | ;;
116 | -f|--force)
117 | force=true
118 | shift
119 | ;;
120 | -rm|--remove)
121 | shift
122 | if [[ "$1" != "" ]]; then
123 | remove="$1"
124 | shift
125 | else
126 | echo "E: Arg missing for -rm option"
127 | exit 1
128 | fi
129 | ;;
130 | -rmd|--remove-with-dir)
131 | shift
132 | if [[ "$1" != "" ]]; then
133 | remove="$1"
134 | rmd=true
135 | shift
136 | else
137 | echo "E: Arg missing for -rm option"
138 | exit 1
139 | fi
140 | ;;
141 | -n|--name)
142 | shift
143 | if [[ "$1" != "" ]]; then
144 | localhost="$1"
145 | shift
146 | else
147 | echo "E: Arg missing for -n option"
148 | exit 1
149 | fi
150 | ;;
151 | -*)
152 | echo "E: Invalid option: $1"
153 | shift
154 | exit 1
155 | ;;
156 | esac
157 | done
158 |
159 | [ "$dr" = "" ] && dr='/var/www/html'
160 |
161 | opts="
162 | ***** Passed options *****
163 | DocumentRoot : $dr
164 | ServerName : $localhost
165 | SSL : $ssl
166 | SSL only : $ssl_only
167 | Install : $install
168 | Uninstall : $uninstall
169 | Reinstall : $reinstall
170 | NoUpdate : $no_update
171 | "
172 | #echo "$opts"
173 |
174 |
175 | #check for root
176 | if [ $EUID -ne 0 ]; then
177 | printf "\n\tRoot privilege required\n"
178 | printf "\tSorry! Exiting\n\n"
179 | exit 1
180 | fi
181 | user=www-data
182 | if [ ! -z "$SUDO_USER" ]; then
183 | user="$SUDO_USER"
184 | fi
185 | mkdir -p "$dr"
186 | chown $user:$user "$dr"
187 | chmod 755 "$dr"
188 |
189 | #extract dist version
190 | rel=(`lsb_release -r | grep -o '[0-9]\+'`)
191 | rv=${rel[0]}
192 |
193 | tphp=''
194 | endm="
195 | ****** Successfully reloaded apache2 ******
196 | "
197 |
198 | aconf=/etc/apache2/apache2.conf
199 |
200 | if [ -n "$remove" ]; then
201 | a2dissite $remove
202 | remove_file=/etc/apache2/sites-available/"$remove"
203 | file=/etc/hosts
204 | pat="^\([[:blank:]]*127\.0\.0\.1.*\)[[:blank:]]$remove\b\(.*\)$"
205 | while grep -e "$pat" "$file" >/dev/null 2>&1;do
206 | sed -i.bak -e "s/$pat/\1\2/" "$file"
207 | done
208 | if [ -f "$remove_file.conf" ]; then
209 | rm_dir="$(sed -n -e 's/^[[:blank:]]*DocumentRoot[[:blank:]]*\([^#]*\).*/\1/p' "$remove_file.conf")"
210 | if $rmd && ! test -z "$rm_dir" && [ "." != "$rm_dir" ] && [ '..' != "$rm_dir" ]; then
211 | rm -r "$rm_dir"
212 | fi
213 | fi
214 | rm "$remove_file.conf" "$remove_file.conf.bak"
215 | if $ssl; then
216 | a2dissite $remove-ssl
217 | rm "$remove_file-ssl.conf" "$remove_file-ssl.conf.bak"
218 | fi
219 | else
220 | if $install || $uninstall; then
221 |
222 | #update
223 | if ! $no_update && ! $uninstall;then apt-get update;fi
224 |
225 | #apache2
226 | list=(
227 | 'apache2'
228 | )
229 |
230 | #php
231 | php='mcrypt'
232 | if (( $rv <= 15 ));then
233 | php="$php php5 libapache2-mod-php5 php5-mcrypt php5-cgi php5-cli php5-common php5-curl php5-gd"
234 | elif (( $rv >= 16 ));then
235 | php="$php php libapache2-mod-php php-mcrypt php-cgi php-cli php-common php-curl php-gd"
236 | fi
237 |
238 | list+=("$php")
239 |
240 | # mysql
241 | mysql='mysql-server mysql-client'
242 | if (( $rv <= 15 ));then
243 | mysql="$mysql php5-mysql"
244 | elif (( $rv >= 16 ));then
245 | mysql="$mysql php-mysql"
246 | fi
247 | list+=("$mysql")
248 |
249 | #phpmyadmin
250 | if ! $npa; then
251 | phpmd='phpmyadmin apache2-utils'
252 | else
253 | phpmd=''
254 | fi
255 | if (( $rv >= 16 ));then
256 | phpmd="$phpmd php-gettext php-mbstring"
257 | fi
258 | list+=("$phpmd")
259 |
260 | for app in "${list[@]}";do
261 | if ! $uninstall; then
262 | echo "###### Installing: $app #######"
263 | { if ! $reinstall; then apt-get -y install $app;else apt-get --force-yes -y install --reinstall $app;fi; } ||
264 | {
265 | echo "W: Failed to install: $app"
266 | [ "$app" = apache2 ] && exit 1
267 | }
268 | else
269 | echo "###### Removing: $app #######"
270 | apt-get -y purge $app
271 | fi
272 |
273 | done
274 | if ! $npa; then
275 | pdconf=/etc/phpmyadmin/apache.conf
276 | pdincl="Include $pdconf"
277 | if ! grep -s -e "$pdincl" "$aconf" >/dev/null 2>&1; then
278 | sed -i.bak "$ a $pdincl" "$aconf" &&
279 | printf "\n\t apache2 conf modified to include phpmyadmin\n" ||
280 | printf "\n\tE: failed to modify apache2 conf for phpmyadmin\n"
281 | else
282 | printf "\n\tphpmyadmin is already added in apache2 conf (skipped)\n"
283 | fi
284 | fi
285 |
286 | apt-get autoremove
287 |
288 | if ! $uninstall; then
289 | if (( $rv <= 15 )); then
290 | php5enmod mcrypt
291 | elif (( $rv >= 16 )); then
292 | phpenmod mcrypt
293 | phpenmod mbstring
294 | fi
295 |
296 | fi
297 |
298 | fi
299 |
300 | #if uninstall exit
301 | if $uninstall;then echo "Removed LAMP installation (custom sites are not removed)."; exit 0;fi
302 |
303 | #create info.php
304 | [ ! -f "$dr"/info.php ] && echo "$tphp" > "$dr"/info.php && printf "\n\tinfo.php created\n" || printf "\n\tinfo.php exists (skipped)\n"
305 | chown $user:$user "$dr"/info.php
306 | chmod 640 "$dr"/info.php
307 |
308 | dire_opts="\n\t\tAllowOverride All\n\t\tOptions Indexes FollowSymLinks MultiViews\n\t\tRequire all granted\n\t"
309 | dire="\t$dire_opts\n"
310 |
311 | #custom sites
312 | sites_av=/etc/apache2/sites-available
313 | ds="$(find "$sites_av" -name '*default.conf')"
314 | ds_ssl="$(find "$sites_av" -name '*default*ssl*.conf')"
315 | dsn="$sites_av/$localhost.conf"
316 | ds_ssln="$sites_av/$localhost-ssl.conf"
317 |
318 | if [ "$ds" = "$dsn" ]; then
319 | printf "$ds\m\t\tand\n$dsn\n\t\tcan not be same\nE: ServerName: $(basename "$ds") not allowed\n"
320 | exit 1
321 | elif [ "$ds_ssl" = "$ds_ssln" ]; then
322 | printf "$ds_ssl\m\t\tand\n$ds_ssln\n\t\tcan not be same\nE: ServerName: default not allowed\n"
323 | exit 1
324 | fi
325 |
326 | export localhost
327 | export dr
328 | export dire
329 | copy_default_config(){
330 | sed -e "s=^\([[:blank:]]*DocumentRoot\).*$=\1 $dr="\
331 | -e "s=^\([[:blank:]]*ServerName\).*$=\1 $localhost="\
332 | -e "s=^\([[:blank:]]*<[[:blank:]]*Directory[[:blank:]]*\)[^[:blank:]]\+\([[:blank:]]*>[[:blank:]]*\)$=\1$dr\2="\
333 | -e "s=^\([[:blank:]]*<[[:blank:]]*VirtualHost[[:blank:]]*\)[^:]*\(:[^>]*>[[:blank:]]*\)$=\1 127.0.0.1\2="\
334 | "$1" > "$2" &&
335 | printf "\n\tcopied $1 --> $2 with required modifications\n" ||
336 | printf "\n\tE: Failed to copy $1 --> $2\n"
337 | }
338 |
339 | if ! $ssl_only; then
340 | copy_default_config "$ds" "$dsn"
341 | fi
342 | #no else
343 | if $ssl; then
344 | copy_default_config "$ds_ssl" "$ds_ssln"
345 | fi
346 |
347 | insert_server_property(){
348 | if ! grep -s -e "^\([[:blank:]]*$1\).*$" "$2" >/dev/null 2>&1; then
349 | sed -i.bak -e "s=^[[:blank:]]*<[[:blank:]]*VirtualHost[[:blank:]]*.*>[[:blank:]]*$=&\n\tServerName $localhost=" "$2" &&
350 | printf "\n\tSuccessfully inserted $1 to $2\n" ||
351 | printf "\n\tE: Failed to insert $1 to $2\n"
352 | else
353 | printf "\n\t$1 exists in $2 (skipped)\n"
354 | fi
355 | }
356 | if ! $ssl_only; then
357 | insert_server_property ServerName "$dsn"
358 | fi
359 | if $ssl; then
360 | insert_server_property ServerName "$ds_ssln"
361 | fi
362 |
363 | insert_docroot_property(){
364 | if ! grep -s -e "^[[:blank:]]*<[[:blank:]]*Directory[[:blank:]]*[^[:blank:]]\+[[:blank:]]*>[[:blank:]]*$" "$1" >/dev/null 2>&1; then
365 | sed -i.bak -e "s=^\([[:blank:]]*\)DocumentRoot.*$=&\n$dire=" "$1" &&
366 | printf "\n\tSuccessfully inserted DocumentRoot in $1\n" ||
367 | printf "\n\tE: Failed to insert DocumentRoot in $1\n"
368 | else
369 | cat "${1-x}" > "${1-x}.bak" 2>&1 &&
370 | printf "\n\tBack up: ${1-x} succeded\n" ||
371 | printf "\n\tE: Back up: ${1-x} failed\n"
372 | echo "$(awk '
373 | BEGIN {p=1}
374 | /^[[:blank:]]*<[[:blank:]]*Directory[[:blank:]]*[^[:blank:]]+[[:blank:]]*>[[:blank:]]*/ {print;print "'"$dire_opts"'";p=0}
375 | /[^#]*<\/Directory/ {p=1}
376 | p' "${1-x}")" > "${1-x}" &&
377 | printf "\n\tSuccessfully edited ${1-x}\n" ||
378 | printf "\n\tE: Failed to edit ${1-x}\n"
379 | fi
380 | }
381 |
382 | if ! $ssl_only; then
383 | insert_docroot_property "$dsn"
384 | fi
385 | if $ssl; then
386 | insert_docroot_property "$ds_ssln"
387 | fi
388 |
389 | #manage /etc/hosts
390 | pat='^[[:blank:]]*127\.0\.0\.1[[:blank:]]\+.*$'
391 | if grep "$pat" /etc/hosts >/dev/null 2>&1; then
392 | sed -i.bak -e "s/$pat/& $localhost/" /etc/hosts >/dev/null &&
393 | printf "\n\tAdded $localhost to /etc/hosts\n" ||
394 | printf "\n\tE: Failed to add $localhost to /etc/hosts\n"
395 | else
396 | sed -i.bak -e "1 a 127.0.0.1 $localhost" /etc/hosts >/dev/null &&
397 | printf "\n\tAdded $localhost to /etc/hosts\n" ||
398 | printf "\n\tE: Failed to add $localhost to /etc/hosts\n"
399 | fi
400 |
401 | #enable mods
402 | a2enmod rewrite &&
403 | printf "\n\tEnabled mod_rewrite\n" ||
404 | printf "\n\tE: Failed to enable mod_rewrite\n"
405 | if $ssl; then
406 | a2enmod ssl &&
407 | printf "\n\tEnabled mod_ssl\n" ||
408 | printf "\n\tE: Failed to enable mod_ssl\n"
409 | fi
410 |
411 | #enable sites
412 | if ! $ssl_only; then
413 | a2ensite "$localhost" &&
414 | printf "\n\tEnabled $localhost\n" ||
415 | printf "\n\tE: Failed to enable $localhost\n"
416 | fi
417 | if $ssl; then
418 | a2ensite "$localhost"-ssl &&
419 | printf "\n\tEnabled $localhost-ssl\n" ||
420 | printf "\n\tE: Failed to enable $localhost-ssl\n"
421 | fi
422 |
423 | endm="$endm
424 | ******* Visit Site *******
425 | "
426 | if ! $ssl_only; then
427 | endm="${endm}http://$localhost/info.php
428 | "
429 | fi
430 | if $ssl; then
431 | endm="${endm}https://$localhost/info.php
432 | "
433 | fi
434 | fi
435 |
436 | #restart the apache2 service
437 | if ! $nsr; then
438 | {
439 | service apache2 restart ||
440 | systemctl restart apache2
441 | } &&
442 | echo "$endm" ||
443 | echo "\n\tE: Failed to restart apache2\n"
444 | else
445 | echo "$endm after restarting the server."
446 | fi
447 |
448 |
--------------------------------------------------------------------------------
/letsacme.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """@package letsacme
3 | ################ letsacme ###################
4 | This script automates the process of getting a signed TLS/SSL certificate
5 | from Let's Encrypt using the ACME protocol. It will need to be run on your
6 | server and have access to your private account key.
7 | It gets both the certificate and the chain (CABUNDLE) and
8 | prints them on stdout unless specified otherwise.
9 | """
10 |
11 | import argparse # argument parser
12 | import subprocess # Popen
13 | import json # json.loads
14 | import os # os.path
15 | import sys # sys.exit
16 | import base64 # b64encode
17 | import binascii # unhexlify
18 | import time # time
19 | import hashlib # sha256
20 | import re # regex operation
21 | import copy # deepcopy
22 | import textwrap # wrap and dedent
23 | import logging # Logger
24 | import errno # EEXIST
25 | import shutil # rmtree
26 |
27 | try: # Python 3
28 | from urllib.request import urlopen
29 | from urllib.request import build_opener
30 | from urllib.request import HTTPRedirectHandler
31 | from urllib.error import HTTPError
32 | from urllib.error import URLError
33 | except ImportError: # Python 2
34 | from urllib2 import urlopen
35 | from urllib2 import HTTPRedirectHandler
36 | from urllib2 import build_opener
37 | from urllib2 import HTTPError
38 | from urllib2 import URLError
39 |
40 | ##################### letsacme info #####################
41 | VERSION = "0.1.3"
42 | VERSION_INFO = "letsacme version: "+VERSION
43 | ##################### API info ##########################
44 | CA_VALID = "https://acme-v01.api.letsencrypt.org"
45 | CA_TEST = "https://acme-staging.api.letsencrypt.org"
46 | TERMS = 'https://acme-v01.api.letsencrypt.org/terms'
47 | API_DIR_NAME = 'directory'
48 | NEW_REG_KEY = 'new-reg'
49 | NEW_CERT_KEY = 'new-cert'
50 | NEW_AUTHZ_KEY = 'new-authz'
51 | ##################### Defaults ##########################
52 | DEFAULT_CA = CA_VALID
53 | API_INFO = set({})
54 | # used as a fallback in DocumentRoot method:
55 | WELL_KNOWN_DIR = ".well-known/acme-challenge"
56 | ##################### Logger ############################
57 | LOGGER = logging.getLogger(__name__)
58 | LOGGER.addHandler(logging.StreamHandler())
59 | LOGGER.setLevel(logging.INFO)
60 | #########################################################
61 |
62 | def error_exit(msg, log):
63 | """Print error message and exit with 1 exit status"""
64 | log.error(msg)
65 | sys.exit(1)
66 |
67 | def get_canonical_url(url, log):
68 | """Follow redirect and return the canonical URL"""
69 | try:
70 | opener = build_opener(HTTPRedirectHandler)
71 | request = opener.open(url)
72 | return request.url
73 | except (URLError, HTTPError) as err:
74 | log.error(str(err))
75 | return url
76 |
77 | def get_boolean_options_from_json(conf_json, ncn, ncrt, tst, frc, quiet):
78 | """Parse config json for boolean options and return them sequentially.
79 | It takes prioritised values as params. Among these values, non-None/True values are
80 | preserved and their values in config json are ignored."""
81 | opt = {'NoChain':ncn, 'NoCert':ncrt, 'Test':tst, 'Force':frc, 'Quiet': quiet}
82 | for key in opt:
83 | if not opt[key] and key in conf_json and conf_json[key].lower() == "true":
84 | opt[key] = True
85 | continue
86 | return opt['NoChain'], opt['NoCert'], opt['Test'], opt['Force'], opt['Quiet']
87 |
88 | def get_options_from_json(conf_json, ack, csr, acmd, crtf, chnf, ca):
89 | """Parse key-value options from config json and return the values sequentially.
90 | It takes prioritised values as params. Among these values, non-None values are
91 | preserved and their values in config json are ignored."""
92 | opt = {'AccountKey':ack, 'CSR':csr, 'AcmeDir':acmd, 'CertFile':crtf, 'ChainFile':chnf, 'CA':ca}
93 | for key in opt:
94 | if not opt[key] and key in conf_json and conf_json[key]:
95 | opt[key] = conf_json[key]
96 | continue
97 | opt[key] = None if opt[key] == '' or opt[key] == '.' or opt[key] == '..' else opt[key]
98 | return opt['AccountKey'], opt['CSR'], opt['AcmeDir'], opt['CertFile'], opt['ChainFile'],\
99 | opt['CA']
100 |
101 | def get_chain(url, log):
102 | """Download chain from chain url and return it"""
103 | resp = urlopen(url)
104 | if resp.getcode() != 200:
105 | error_exit("E: Failed to fetch chain (CABUNDLE) from: "+url, log)
106 | return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
107 | "\n".join(textwrap.wrap(base64.b64encode(resp.read()).decode('utf8'), 64)))
108 |
109 | def write_file(path, content, log, exc=True):
110 | """Write content to the file specified by path"""
111 | try:
112 | with open(path, "w") as fileh:
113 | fileh.write(content)
114 | except IOError as err:
115 | log.exception("I/O error.")
116 | if exc:
117 | raise
118 |
119 |
120 | def get_crt(account_key, csr, conf_json, well_known_dir, acme_dir, log, CA, force):
121 | """Register account, parse CSR, complete challenges and finally
122 | get the signed SSL certificate and return it."""
123 | def _b64(bcont):
124 | """helper function base64 encode for jose spec"""
125 | return base64.urlsafe_b64encode(bcont).decode('utf8').replace("=", "")
126 |
127 | def make_dirs(path):
128 | """Make directories including parent directories (if not exist)"""
129 | try:
130 | os.makedirs(path)
131 | except OSError as err:
132 | if err.errno != errno.EEXIST:
133 | log.exception("Exception in make_drs")
134 | raise
135 |
136 | # get challenge directory from json by domain name
137 | def get_challenge_dir(conf_json, dom, acmed):
138 | """Get the challenge directory path from config json"""
139 | if conf_json:
140 | if dom not in conf_json:
141 | if re.match(r'www[^.]*\.', dom):
142 | dom1 = re.sub(r"^www[^.]*\.", "", dom)
143 | else:
144 | dom1 = "www."+dom
145 | if dom1 in conf_json:
146 | dom = dom1
147 | # no else
148 | if dom in conf_json:
149 | if 'AcmeDir' in conf_json[dom]:
150 | return None, conf_json[dom]['AcmeDir']
151 | elif 'DocumentRoot' in conf_json[dom]:
152 | return conf_json[dom]['DocumentRoot'], None
153 | # if none is given we will try to take challenge dir from global options
154 | if 'AcmeDir' in conf_json:
155 | return None, conf_json['AcmeDir']
156 | elif 'DocumentRoot' in conf_json:
157 | return conf_json['DocumentRoot'], None
158 | elif acmed:
159 | return None, acmed
160 | else:
161 | error_exit("E: There is no valid entry for \"DocumentRoot\" or \"AcmeDir\" for \
162 | the domain '"+dom+"' in\n" +
163 | json.dumps(conf_json, indent=4, sort_keys=True), log)
164 |
165 | # parse account key to get public key
166 | log.info("Parsing account key...")
167 | proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"],
168 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
169 | out, err = proc.communicate()
170 | if proc.returncode != 0:
171 | error_exit("\tE: OpenSSL Error: {0}".format(err), log)
172 | pub_hex, pub_exp = re.search(
173 | r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
174 | out.decode('utf8'), re.MULTILINE|re.DOTALL).groups()
175 | pub_exp = "{0:x}".format(int(pub_exp))
176 | pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
177 | header = {
178 | "alg": "RS256",
179 | "jwk": {
180 | "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
181 | "kty": "RSA",
182 | "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
183 | },
184 | }
185 | accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':'))
186 | thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
187 | log.info('\tParsed!')
188 |
189 | # helper function make signed requests
190 | def _send_signed_request(url, payload):
191 | payload64 = _b64(json.dumps(payload).encode('utf8'))
192 | protected = copy.deepcopy(header)
193 | protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce']
194 | protected64 = _b64(json.dumps(protected).encode('utf8'))
195 | proc = subprocess.Popen(["openssl", "dgst", "-sha256", "-sign", account_key],
196 | stdin=subprocess.PIPE,
197 | stdout=subprocess.PIPE,
198 | stderr=subprocess.PIPE)
199 | out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8'))
200 | if proc.returncode != 0:
201 | error_exit("E: OpenSSL Error: {0}".format(err), log)
202 | data = json.dumps({
203 | "header": header, "protected": protected64,
204 | "payload": payload64, "signature": _b64(out),
205 | })
206 | try:
207 | resp = urlopen(url, data.encode('utf8'))
208 | return resp.getcode(), resp.read(), resp.info()
209 | except IOError as err:
210 | return getattr(err, "code", None), getattr(err, "read", err.__str__),\
211 | getattr(err, "info", None)()
212 |
213 | crt_info = set([])
214 |
215 | # find domains
216 | log.info("Parsing CSR...")
217 | proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"],
218 | stdout=subprocess.PIPE, stderr=subprocess.PIPE)
219 | out, err = proc.communicate()
220 | if proc.returncode != 0:
221 | error_exit("\tE: Error loading {0}: {1}".format(csr, err), log)
222 | domains = set([])
223 | common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8'))
224 | if common_name is not None:
225 | domains.add(common_name.group(1))
226 | log.info("\tCN: "+common_name.group(1))
227 | subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n",
228 | out.decode('utf8'), re.MULTILINE|re.DOTALL)
229 | if subject_alt_names is not None:
230 | for san in subject_alt_names.group(1).split(", "):
231 | if san.startswith("DNS:"):
232 | domains.add(san[4:])
233 | log.info('\tParsed!')
234 |
235 | # get the certificate domains and expiration
236 | log.info("Registering account...")
237 | agreement_url = get_canonical_url(TERMS, log)
238 | code, result, crt_info = _send_signed_request(API_INFO[NEW_REG_KEY], {
239 | "resource": NEW_REG_KEY,
240 | "agreement": agreement_url,
241 | })
242 | if code == 201:
243 | log.info("\tRegistered!")
244 | elif code == 409:
245 | log.info("\tAlready registered!")
246 | else:
247 | error_exit("\tE: Error registering: {0} {1}".format(code, result), log)
248 | # verify each domain
249 | for domain in domains:
250 | log.info("Verifying {0}...".format(domain))
251 |
252 | # get new challenge
253 | code, result, crt_info = _send_signed_request(API_INFO[NEW_AUTHZ_KEY], {
254 | "resource": NEW_AUTHZ_KEY,
255 | "identifier": {"type": "dns", "value": domain},
256 | })
257 | if code != 201:
258 | error_exit("\tE: Error requesting challenges: {0} {1}".format(code, result), log)
259 |
260 | # create the challenge file
261 | challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] \
262 | if c['type'] == "http-01"][0]
263 | token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
264 | keyauthorization = "{0}.{1}".format(token, thumbprint)
265 | wellknown_url = None
266 | if 'validationRecord' in challenge:
267 | for item in challenge['validationRecord']:
268 | if 'url' in item:
269 | res_m = re.match('.*://'+domain+r'/([\w\W]*)/'+token, item['url'])
270 | if res_m:
271 | well_known_dir = res_m.group(1)
272 | wellknown_url = res_m.group(0)
273 | log.info('\tWell known path was parsed: '+well_known_dir)
274 | # paranoid check
275 | if os.path.sep in token or (os.path.altsep or '\\') in token or not token:
276 | error_exit("\tE: Invalid and possibly dangerous token.", log)
277 | # take either acme-dir or document dir method
278 | doc_root, acme_dir = get_challenge_dir(conf_json, domain, acme_dir)
279 | if acme_dir:
280 | chlng = acme_dir.rstrip(os.path.sep+(os.path.altsep or "\\"))
281 | make_dirs(chlng)
282 | wellknown_path = os.path.join(chlng, token)
283 | elif doc_root:
284 | doc_root = doc_root.rstrip(os.path.sep+(os.path.altsep or "\\"))
285 | chlng = os.path.join(doc_root, well_known_dir.strip(os.path.sep+\
286 | (os.path.altsep or "\\")))
287 | make_dirs(chlng)
288 | wellknown_path = os.path.join(chlng, token)
289 | else:
290 | error_exit("\tE: Couldn't get DocumentRoot or AcmeDir for domain: "+domain, log)
291 | # another paranoid check
292 | if os.path.isdir(wellknown_path):
293 | log.warning("\tW: "+wellknown_path+" exists.")
294 | try:
295 | os.rmdir(wellknown_path)
296 | except OSError:
297 | if force:
298 | try:
299 | # This is why we have done paranoid check on token
300 | shutil.rmtree(wellknown_path)
301 | # though it itself is inside a paranoid check
302 | # which will probably never be reached
303 | log.info("\tRemoved "+wellknown_path)
304 | except OSError as err:
305 | error_exit("\tE: Failed to remove "+wellknown_path+'\n'+str(err), log)
306 | else:
307 | error_exit("\tE: "+wellknown_path+" is a directory. \
308 | It shouldn't even exist in normal cases. \
309 | Try --force option if you are sure about \
310 | deleting it and all of its' content", log)
311 |
312 | write_file(wellknown_path, keyauthorization, log)
313 |
314 | # check that the file is in place
315 | if not wellknown_url:
316 | wellknown_url = ("http://{0}/"+well_known_dir+"/{1}").format(domain, token)
317 | try:
318 | resp = urlopen(wellknown_url)
319 | resp_data = resp.read().decode('utf8').strip()
320 | assert resp_data == keyauthorization
321 | except (IOError, AssertionError):
322 | os.remove(wellknown_path)
323 | log.critical("\tE: Wrote file to {0}, but couldn't download {1}".format(\
324 | wellknown_path, wellknown_url,))
325 | raise
326 |
327 | # notify challenge is met
328 | code, result, crt_info = _send_signed_request(challenge['uri'], {
329 | "resource": "challenge",
330 | "keyAuthorization": keyauthorization,
331 | })
332 | if code != 202:
333 | os.remove(wellknown_path)
334 | error_exit("\tE: Error triggering challenge: {0} {1}".format(code, result.read()), log)
335 |
336 | # wait for challenge to be verified
337 | while True:
338 | try:
339 | resp = urlopen(challenge['uri'])
340 | challenge_status = json.loads(resp.read().decode('utf8'))
341 | except IOError as err:
342 | os.remove(wellknown_path)
343 | log.critical("\tE: Error checking challenge: {0} {1}\n{2}".format(\
344 | resp.code, json.dumps(resp.read().decode('utf8'), indent=4), str(err),))
345 | raise
346 | if challenge_status['status'] == "pending":
347 | time.sleep(1)
348 | elif challenge_status['status'] == "valid":
349 | os.remove(wellknown_path)
350 | log.info("\tverified!")
351 | break
352 | else:
353 | os.remove(wellknown_path)
354 | error_exit("\tE: {0} challenge did not pass: {1}".format(\
355 | domain, challenge_status), log)
356 |
357 | # get the new certificate
358 | test_mode = " (test mode)" if CA == CA_TEST else ""
359 | log.info("Signing certificate..."+test_mode)
360 | proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"],
361 | stdout=subprocess.PIPE, stderr=subprocess.PIPE)
362 | csr_der, err = proc.communicate()
363 | code, result, crt_info = _send_signed_request(API_INFO[NEW_CERT_KEY], {
364 | "resource": NEW_CERT_KEY,
365 | "csr": _b64(csr_der),
366 | })
367 | if code != 201:
368 | error_exit("\tE: Error signing certificate: {0} {1}".format(code, result), log)
369 |
370 | log.info('\tParsing chain url...')
371 | res_m = re.match("\\s*<([^>]+)>;rel=\"up\"", crt_info['Link'])
372 | chain_url = res_m.group(1) if res_m else None
373 | if not chain_url:
374 | log.error('\tW: Failed to parse chain url!')
375 |
376 | # return signed certificate!
377 | log.info("\tSigned!"+test_mode)
378 | return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
379 | "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))), chain_url
380 |
381 | def main(argv):
382 | """Parse arguments and run helper functions to get the certs"""
383 | parser = argparse.ArgumentParser(
384 | formatter_class=argparse.RawDescriptionHelpFormatter,
385 | description=textwrap.dedent("""\
386 | This script automates the process of getting a signed TLS/SSL certificate from
387 | Let's Encrypt using the ACME protocol. It will need to be run on your server
388 | and have access to your private account key, so PLEASE READ THROUGH IT!.
389 |
390 | ===Example Usage===
391 | python letsacme.py --config-json /path/to/config.json
392 | ===================
393 |
394 | ===Example Crontab Renewal (once per month)===
395 | 0 0 1 * * python /path/to/letsacme.py --config-json /path/to/config.json > /path/to/full-chain.crt 2>> /path/to/letsacme.log
396 | ==============================================
397 | """)
398 | )
399 | parser.add_argument("--account-key", help="Path to your Let's Encrypt account private key.")
400 | parser.add_argument("--csr", help="Path to your certificate signing request.")
401 | parser.add_argument("--config-json", default=None, help="Configuration JSON string/file. \
402 | Must contain \"DocumentRoot\":\"/path/to/document/root\" entry \
403 | for each domain.")
404 | parser.add_argument("--acme-dir", default=None, help="Path to the acme challenge directory")
405 | parser.add_argument("--cert-file", default=None, help="File to write the certificate to. \
406 | Overwrites if file exists.")
407 | parser.add_argument("--chain-file", default=None, help="File to write the certificate to. \
408 | Overwrites if file exists.")
409 | parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="Suppress \
410 | output except for errors.")
411 | parser.add_argument("--ca", default=None, help="Certificate authority, default is Let's \
412 | Encrypt.")
413 | parser.add_argument("--no-chain", action="store_true", help="Fetch chain (CABUNDLE) but\
414 | do not print it on stdout.")
415 | parser.add_argument("--no-cert", action="store_true", help="Fetch certificate but do not\
416 | print it on stdout.")
417 | parser.add_argument("--force", action="store_true", help="Apply force. If a directory\
418 | is found inside the challenge directory with the same name as\
419 | challenge token (paranoid), this option will delete the directory\
420 | and it's content (Use with care).")
421 | parser.add_argument("--test", action="store_true", help="Get test certificate (Invalid \
422 | certificate). This option won't have any effect if --ca is passed.")
423 | parser.add_argument("--version", action="version", version=VERSION_INFO, help="Show version \
424 | info.")
425 |
426 | args = parser.parse_args(argv)
427 | if not args.config_json and not args.acme_dir:
428 | parser.error("One of --config-json or --acme-dir must be given")
429 |
430 | # parse config_json
431 | conf_json = None
432 | if args.config_json:
433 | config_json_s = args.config_json
434 | # config_json can point to a file too.
435 | if os.path.isfile(args.config_json):
436 | try:
437 | with open(args.config_json, "r") as fileh:
438 | config_json_s = fileh.read()
439 | except IOError as err:
440 | LOGGER.critical("E: Failed to read json file: "+args.config_json)
441 | raise
442 | # Now we are sure that config_json_s is a json string, not file
443 | try:
444 | conf_json = json.loads(config_json_s)
445 | except ValueError as err:
446 | LOGGER.critical("E: Failed to parse json")
447 | raise
448 | args.account_key, args.csr, args.acme_dir, args.cert_file,\
449 | args.chain_file, args.ca = get_options_from_json(conf_json,
450 | args.account_key,
451 | args.csr,
452 | args.acme_dir,
453 | args.cert_file,
454 | args.chain_file,
455 | args.ca)
456 | args.no_chain, args.no_cert, args.test, args.force, args.quiet = \
457 | get_boolean_options_from_json(conf_json, args.no_chain, args.no_cert,
458 | args.test, args.force, args.quiet)
459 |
460 | LOGGER.setLevel(logging.ERROR if args.quiet else LOGGER.level)
461 |
462 | # show error in case args are missing
463 | if not args.account_key:
464 | error_exit("E: Account key path not specified.", LOGGER)
465 | if not args.csr:
466 | error_exit("E: CSR path not specified", LOGGER)
467 | if not args.config_json and not args.acme_dir:
468 | error_exit("E: Either --acme-dir or --config-json must be given", log=LOGGER)
469 | # we need to set a default CA if not specified
470 | if not args.ca:
471 | args.ca = CA_TEST if args.test else DEFAULT_CA
472 |
473 | global API_INFO # this is where we will pull our information from
474 | API_INFO = json.loads(urlopen(args.ca+'/'+API_DIR_NAME).read().decode('utf8'))
475 |
476 | # lets do the main task
477 | signed_crt, chain_url = get_crt(args.account_key, args.csr,
478 | conf_json, well_known_dir=WELL_KNOWN_DIR,
479 | acme_dir=args.acme_dir, log=LOGGER,
480 | CA=args.ca, force=args.force)
481 |
482 | if args.cert_file:
483 | write_file(args.cert_file, signed_crt, LOGGER, False)
484 | if not args.no_cert:
485 | sys.stdout.write(signed_crt)
486 |
487 | if chain_url:
488 | chain = get_chain(chain_url, log=LOGGER)
489 | if args.chain_file:
490 | write_file(args.chain_file, chain, LOGGER, False)
491 | if not args.no_chain:
492 | sys.stdout.write(chain)
493 |
494 | if __name__ == "__main__": # pragma: no cover
495 | main(sys.argv[1:])
496 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | PROJECT ABANDONED
2 | -----------------
3 |
4 | **This project is outdated and orphaned. Please use selectively the acme_tiny method described in this readme for both shared hosting and dedicated hosting.**
5 |
6 | The **letsacme** script automates the process of getting a signed TLS/SSL certificate from Let's Encrypt using the ACME protocol. It will need to be run on your server and have **access to your private Let's Encrypt account key**. It gets both the certificate and the chain (CABUNDLE) and prints them on stdout unless specified otherwise.
7 |
8 | **PLEASE READ THE SOURCE CODE! YOU MUST TRUST IT WITH YOUR PRIVATE LET'S ENCRYPT ACCOUNT KEY!**
9 |
10 | # Dependencies:
11 | 1. Python
12 | 2. openssl
13 |
14 | # How to use:
15 |
16 | If you just want to renew an existing certificate, you will only have to do Steps 4~6.
17 |
18 | **For shared servers/hosting:** Get only the certificate (step 1~4) by running the script on your server and then install the certificate with cpanel or equivalent control panels. If you don't want to go all technical about it and just want to follow a step by step process to get the certificate, then [this tutorial](https://neurobin.org/docs/web/letsacme/get-letsencrypt-certficate-for-shared-hosting/) may be the right choice for you.
19 |
20 | If you are on a cpanel hosting then [this tutorial](https://neurobin.org/docs/web/fully-automated-letsencrypt-integration-with-cpanel/) will help you automate the whole process.
21 |
22 | ## 1: Create a Let's Encrypt account private key (if you haven't already):
23 | You must have a public key registered with Let's Encrypt and use the corresponding private key to sign your requests. Thus you first need to create a key, which **letsacme** will use to register an account for you and sign all the following requests.
24 |
25 | If you don't understand what the account is for, then this script likely isn't for you. Please, use the official Let's Encrypt client. Or you can read the [howitworks](https://letsencrypt.com/howitworks/technology/) page under the section: **Certificate Issuance and Revocation** to gain a little insight on how certificate issuance works.
26 |
27 | The following command creates a 4096bit RSA (private) key:
28 | ```sh
29 | openssl genrsa 4096 > account.key
30 | ```
31 |
32 | **Or use an existing Let's Encrypt key (privkey.pem from official Let's Encrypt client)**
33 |
34 | **Note:** **letsacme** is using the [PEM](https://tools.ietf.org/html/rfc1421) key format.
35 |
36 | ## 2: Create a certificate signing request (CSR) for your domains.
37 |
38 | The ACME protocol used by Let's Encrypt requires a CSR to be submitted to it (even for renewal). Once you have the CSR, you can use the same CSR as many times as you want. You can create a CSR from the terminal which will require you to create a domain key first, or you can create it from the control panel provided by your hosting provider (for example: cpanel). Whatever method you may use to create the CSR, the script doesn't require the domain key, only the CSR.
39 |
40 | **Note:** Domain key is not the private key you previously created.
41 |
42 | ### An example of creating CSR using openssl:
43 | The following command will create a 4096bit RSA (domain) key:
44 | ```sh
45 | openssl genrsa 4096 > domain.key
46 | ```
47 | Now to create a CSR for a single domain:
48 | ```sh
49 | openssl req -new -sha256 -key domain.key -subj "/CN=example.com" > domain.csr
50 | ```
51 | For multi-domain:
52 | ```sh
53 | openssl req -new -sha256 -key domain.key -subj "/C=US/ST=CA/O=MY Org/CN=example.com" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:example.com,DNS:www.example.com,DNS:subdomain.example.com,DNS:www.subdomain.com")) -out domain.csr
54 | ```
55 | It would probably be easier if you use [gencsr](https://github.com/neurobin/gencsr) to create CSR for multiple domains.
56 |
57 | ## 3: Prepare the challenge directory/s:
58 | **letsacme** provides two methods to prepare the challenge directory/s to complete the acme challenges. One of them is the same as [acme-tiny](https://github.com/diafygi/acme-tiny) (with `--acme-dir`), the other is quite different and simplifies things for users who doesn't have full access to their servers i.e for shared servers or shared hosting.
59 |
60 | **Whatever method you use, note that the challenge directory needs to be accessible with normal http on port 80.**
61 |
62 | Otherwise, you may get an **error message** like this one:
63 | ```sh
64 | Wrote file to /var/www/public_html/.well-known/acme-challenge/rgGoLnQ8VkBOPyXZn-PkPD-A3KH4_2biYVOxbrYRDuQ, but couldn't download http://example.com/.well-known/acme-challenge/rgGoLnQ8VkBOPyXZn-PkPD-A3KH4_2biYVOxbrYRDuQ
65 | ```
66 | See section 3.3 on how you can work this around.
67 |
68 | ### 3.1: Using acme-dir as in acme-tiny (method 1):
69 | This method is the same as acme-tiny except the fact that letsacme prints a fullchain (cert+chain) on stdout (by default), while acme-tiny prints only the cert. If you provide `--no-chain` option (or equivalent `"NoChain": "False"` in config json) then the output will match that of acme-tiny.
70 |
71 | You can pass the acme-dir with `--acme-dir` option or define AcmeDir in json file like `"AcmeDir": "/path/to/acme/dir"`.
72 |
73 | This is how you can prepare an acme-dir:
74 | ```sh
75 | #make some challenge directory (modify to suit your needs)
76 | mkdir -p /var/www/challenges/
77 | ```
78 | Then you need to configure your server.
79 |
80 | Example for nginx (copied from acme-tiny readme):
81 | ```nginx
82 | server {
83 | listen 80;
84 | server_name yoursite.com www.yoursite.com;
85 |
86 | location /.well-known/acme-challenge/ {
87 | alias /var/www/challenges/;
88 | try_files $uri =404;
89 | }
90 |
91 | ...the rest of your config
92 | }
93 | ```
94 | On apache2 you can set Aliases:
95 | ```apache
96 | Alias /.well-known/acme-challenge /var/www/challenges
97 | ```
98 |
99 | **You can't use this method on shared server** as most of the shared server won't allow Aliases in AccessFile. For shared server/hosting, you should either use your site's document root as the destination for acme-challenges, or redirect the challenges to a different directory which has a valid and active URL and allows http file download without hindrance. Follow the steps mentioned in section 3.3 to do that.
100 |
101 | ### 3.2: Using Document Root of each of your sites (method 2):
102 | This method (using document root) is different than the acme-tiny script which this script is based on. Acme-tiny requires you to configure your server for completing the challenge; contrary to that, the intention behind this method is to not have to do anything at all on the server configuration until we finally get the certificate. Instead of setting up your server, you can provide the document root (or path to acme challenge directory) of each domain in a JSON format. It will create the *.well-known/acme-challenge* directory under document root (if not exists already) and put the temporary challenge files there.
103 |
104 | **For sites using Wordpress or framework like Laravel, the use of document root as the destination for challenge directory may or may not work. Use the method described in section 3.3 (or section 3.2 if you have full access to the server)**
105 |
106 | To pass document root for each of your domain/subdomain you will need create a json file like this:
107 |
108 | **config.json:**
109 | ```json
110 | {
111 | "example.com": {
112 | "DocumentRoot":"/var/www/public_html"
113 | },
114 | "subdomain1.example.com": {
115 | "DocumentRoot":"/var/www/subdomain1"
116 | },
117 | "subdomain2.example.com": {
118 | "DocumentRoot":"/var/www/subdomain2"
119 | },
120 | "subdomain3.example.com": {
121 | "DocumentRoot":"/var/www/subdomain3"
122 | },
123 | "subdomain4.example.com": {
124 | "DocumentRoot":"/var/www/subdomain4"
125 | },
126 | "subdomain5.example.com": {
127 | "DocumentRoot":"/var/www/subdomain5"
128 | }
129 | }
130 | ```
131 |
132 | **Note:** You can pass all other options in this config json too. see Options for more details.
133 |
134 |
135 |
136 | ### 3.3 What will you do if the challenge directory/document root doesn't allow normal http on port 80 (workaround):
137 |
138 | **The challenge directory must be accessible with normal http on port 80.**
139 |
140 | But this may not be possible all the time. So, what will you do?
141 |
142 | And also there's another scenario: if it happens that your site is behind a firewall or your WordPress site or site with Laravel or other tools and framework is preventing direct access to that challenge directory, what will you do?
143 |
144 | In the above cases most commonly you will be encountered with an error message like this:
145 |
146 | >Wrote file to /var/www/public_html/.well-known/acme-challenge/rgGoLnQ8VkBOPyXZn-PkPD-A3KH4_2biYVOxbrYRDuQ, but couldn't download http://example.com/.well-known/acme-challenge/rgGoLnQ8VkBOPyXZn-PkPD-A3KH4_2biYVOxbrYRDuQ
147 |
148 | This means what it **exactly means**, it can't access the challenge files on the URL. It is either being redirected in a weird way or being blocked.
149 |
150 | You can however work this around with an effective but peculiar way:
151 |
152 | > The basic logic is to redirect all requests to http://example.com/.well-know/acme-challenge/ to another address which permits http access on port 80 and you have access to it's document root (because the script needs to create challenge files there) through terminal.
153 |
154 | Create a subdomain (or use an existing one with no additional framework, just plain old http site). Check if the subdomain is accessible (by creating a simple html file inside). Create a directory named `challenge` inside it's document root (don't use `.well-known/acme-challenge` instead of `challenge`, it will create an infinite loop if this new subdomain also contains the following line of redirection code). And then redirect all *.well-know/acme-challenge* requests to all of the domains you want certificate for to this directory of this new subdomain. A mod_rewrite rule for apache2 would be (add it in the .htaccess file or whatever AccessFile you have):
155 | ```apache
156 | RewriteRule ^.well-known/acme-challenge/(.*)$ http://challenge.example.com/challenge/$1 [L,R=302]
157 | ## If you have any rule that redirects http to https, make sure this rule stays above that one
158 | ## To keep it simple, add this rule above all other rewrite rules.
159 | ```
160 | And provide the challenge directory as acme-dir (not document root) by either `--acme-dir` option or defining `AcmeDir` json property in config json i.e `--acme-dir /var/www/challenge` or using global acme-dir definition in config json:
161 |
162 | ```json
163 | "AcmeDir": "/var/www/challenge"
164 | ```
165 | **Do not install SSL on this site. If you do, at least do not redirect http to https, otherwise it won't work.**
166 |
167 | **Even though it's peculiar and a bit tedious, it is supposed to work with all the situations** as long as the subdomain is properly active. So if you want to move to this method instead of all the other methods available, that wouldn't be a bad idea at all.
168 |
169 | ## 4: Get a signed certificate:
170 | To get a signed certificate, all you need is the private key, the CSR, the JSON configuration file (optional) and a single line of python command (**one of the commands mentioned below**, choose according to your requirements).
171 |
172 | If you created a *config.json* (it contains all options) file:
173 | ```sh
174 | python letsacme.py --config-json ./config.json > ./fullchain.crt
175 | ```
176 | If you didn't create the config.json file and want to use acme-dir, then:
177 | ```sh
178 | python letsacme.py --no-chain --account-key ./account.key --csr ./domain.csr --acme-dir /path/to/acme-dir > ./signed.crt
179 | ```
180 | Notice the `--no-chain` option; if you omitted this option then you would get a fullchain (cert+chain). Also, you can get the chain, cert and fullchain separately:
181 |
182 | ```sh
183 | python letsacme.py --account-key ./account.key --csr ./domain.csr --acme-dir /path/to/acme-dir --cert-file ./signed.cert --chain-file ./chain.crt > ./fullchain.crt
184 | ```
185 | This will create three files: **signed.crt** (the certificate), **chain.crt** (chain), **fullchain.crt** (fullchain).
186 |
187 |
188 | # 5: Install the certificate:
189 | This is an scope beyond the script. You will have to install the certificate manually and setup the server as it requires.
190 |
191 | An example for nginx (nginx requires the *fullchain.crt*):
192 |
193 | ```nginx
194 | server {
195 | listen 443;
196 | server_name example.com, www.example.com;
197 |
198 | ssl on;
199 | ssl_certificate /path/to/fullchain.crt;
200 | ssl_certificate_key /path/to/domain.key;
201 | ssl_session_timeout 5m;
202 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
203 | ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
204 | ssl_session_cache shared:SSL:50m;
205 | ssl_dhparam /path/to/server.dhparam;
206 | ssl_prefer_server_ciphers on;
207 |
208 | ...the rest of your config
209 | }
210 | ```
211 | An example for apache2:
212 | ```apache
213 |
214 | ...other configurations
215 | SSLEngine on
216 | SSLCertificateKeyFile /path/to/domain.key
217 | SSLCertificateFile /path/to/signed.crt
218 | SSLCertificateChainFile /path/to/chain.crt
219 |
220 | ```
221 |
222 | **For shared servers, it is possible to install the certificate with cpanel or equivalent control panels (if it's supported).** [See this link for how to install it with cpanel](https://neurobin.org/docs/web/installing-tls-ssl-certificate-using-cpanel/)
223 |
224 | # 6: Setup an auto-renew cron job:
225 | Let's Encrypt certificate only lasts for 90 days. So you need to renew it in a timely manner. You can setup a cron job to do this for you. An example monthly cron job:
226 | ```sh
227 | 0 0 1 * * /usr/bin/python /path/to/letsacme.py --account-key /path/to/account.key --csr /path/to/domain.csr --config-json /path/to/config.json --cert-file /path/to/signed.crt --chain-file /path/to/chain.crt > /path/to/fullchain.crt 2>> /var/log/letsacme.log && service apache2 restart
228 | ```
229 | But the above code is not recommended as it only tries for once in a month. It may not be enough to renew the certificate on such few tries as they can be timed out due to heavy load or network failures or outage. Let's employ a little retry mechanism. First we need a dedicated script for this:
230 | ```sh
231 | #!/bin/sh
232 | while true;do
233 | if /usr/bin/python /path/to/letsacme.py --account-key /path/to/account.key \
234 | --csr /path/to/domain.csr \
235 | --acme-dir /path/to/acme-dir \
236 | --cert-file /path/to/signed.crt \
237 | --chain-file /path/to/chain.crt \
238 | > /path/to/fullchain.crt \
239 | 2>> /path/to/letsacme.log
240 | then
241 | # echo "Successfully renewed certificate"
242 | service apache2 restart
243 | break
244 | else
245 | sleep `tr -cd 0-9 /path/to/fullchain1.crt 2>> /var/log/letsacme.log
264 | ```
265 | The above cron job runs the command once every day at a random time as it has to wait until perl gets its' sleep (max range 12 hours (43200s)).
266 |
267 | Instead of using the long command, it will be much more readable and easy to maintain if you put those codes into a script and call that script instead:
268 | ```sh
269 | /usr/bin/python /path/to/letsacme.py --account-key /path/to/account.key \
270 | --csr /path/to/domain.csr \
271 | --acme-dir /path/to/acme-dir \
272 | --cert-file /path/to/signed1.crt \
273 | --chain-file /path/to/chain1.crt \
274 | > /path/to/fullchain1.crt \
275 | 2>> /path/to/letsacme.log
276 | ```
277 | cron:
278 | ```sh
279 | 0 12 * * * /usr/local/bin/perl -le 'sleep rand 43200' && /bin/sh /path/to/script
280 | ```
281 |
282 | **Notice** that the names for the crt files are different (\*1.crt) and there's no server restart command.
283 |
284 | We are renewing the certificate every day but that doesn't mean we have to install it every day and restart the server along with it. We can graciously wait for 60 days and then install the certificate as the certificate (\*1.crt) is always renewed. We just need to overwrite the existing one with \*1.crt's. To do that you can set up another cron to overwrite old crt's with new ones and restart the server at a 60 day interval.
285 | ```sh
286 | 0 0 1 */2 * /bin/cp /old/crt/path/signed1.crt /old/crt/path/signed.crt && /bin/cp /old/crt/path/fullchain1.crt /old/crt/path/fullchain.crt && service apache2 restart
287 | ```
288 |
289 | # Permissions:
290 |
291 | 1. **Challenge directory:** The script needs **write permission** to the challenge directory (document root or acme-dir). If writing into document root seems to be a security issue then you can work it around by creating the challenge directories first. If the challenge directory already exists it will only need permission to write to the challenge directory not the document root. The acme-dir method needs **write permission** to the directory specified by `--acme-dir`.
292 | 2. **Account key:** Save the *account.key* file to a secure location. **letsacme** only needs **read permission** to it.
293 | 3. **Domain key:** Save the *domain.key* file to a secure location. **letsacme** doesn't use this file. So **no permission** should be allowed for this file.
294 | 4. **Cert files:** Save the *signed.crt*, *chain.crt* and *fullchain.crt* in a secure location. **letsacme** needs **write permission** for these files as it will update these files in a timely basis.
295 | 5. **Config json:** Save it in a secure location. **letsacme** needs only **read permission** to this file.
296 |
297 | As you will want to secure yourself as much as you can and thus give as less permission as possible to the script, I suggest you create an extra user for this script and give that user write permission to the challenge directory and the cert files, and read permission to the private key (*account.key*) and the config file (*config.json*) and nothing else.
298 |
299 | # Options:
300 |
301 | Run it with `-h` flag to get help and view the options.
302 | ```sh
303 | python letsacme.py -h
304 | ```
305 | It will show you all the available options that are supported by the script. These are the options currently supported:
306 |
307 | Command line option | Equivalent JSON | Details
308 | ---------- | ------------- | -------
309 | `-h`, `--help` | N/A | show this help message and exit
310 | `--account-key PATH` | `"AccountKey": "PATH"` | Path to your Let's Encrypt account private key.
311 | `--csr PATH` | `"CSR": "PATH"` | Path to your certificate signing request.
312 | `--config-json PATH/JSON_STRING` | N/A |Configuration JSON string/file.
313 | `--acme-dir PATH` | `"AcmeDir": "PATH"` | Path to the .well-known/acme-challenge/ directory
314 | `--cert-file PATH` | `"CertFile": "PATH"` | File to write the certificate to. Overwrites if file exists.
315 | `--chain-file PATH` | `"ChainFile": "PATH"` | File to write the certificate to. Overwrites if file exists.
316 | `--quiet` | `"Quiet": "True/False"` | Suppress output except for errors.
317 | `--ca URL` | `"CA": "URL"` | Certificate authority, default is Let's Encrypt.
318 | `--no-chain` | `"NoChain": "True/False"` | Fetch chain (CABUNDLE) but do not print it on stdout.
319 | `--no-cert` | `"NoCert": "True/False"` | Fetch certificate but do not print it on stdout.
320 | `--force` | `"Force: "True/False"` | Apply force. If a directory is found inside the challenge directory with the same name as challenge token (paranoid), this option will delete the directory and it's content (Use with care).
321 | `--test` | `"Test": "True/False"` | Get test certificate (Invalid certificate). This option won't have any effect if --ca is passed.
322 | `--version` | N/A | Show version info.
323 |
324 |
325 | # Passing options:
326 |
327 | You can either pass the options directly as command line parameters when you run the script or save the options in a configuration json file and pass the path to the configuration file with `--config-json`. The `--config-json` option can also take a raw json string instead of a file path.
328 |
329 | If you want to use the acme-dir method, then there's no need to use the config json unless you want to keep your options together, saved somewhere and shorten the command that will be run. But if you use document root as the challenge directory, it is a must to define them in a config json.
330 |
331 | **Most of the previous examples show how you can use it without config json**, and this is how you use letsacme with a configuration json:
332 |
333 | ```sh
334 | python letsacme.py --config-json /path/to/config.json
335 | ```
336 | Now letsacme will take all options from that configuration file. `--config-json` can also take a raw JSON string. This may come in handy if you are writing a script and you don't want to create a separate file to put the JSON. In that case, you can put it in a variable and pass the variable as the parameter:
337 |
338 | ```sh
339 | python letsacme.py --config-json "$conf_json"
340 | ```
341 |
342 |
343 |
344 | # Advanced info about the configuration file:
345 |
346 | 1. Arguments passed in the command line takes priority over properties/options defined in the JSON file.
347 | 2. DocumentRoot and AcmeDir can be defined both globally and locally (per domain basis). If any local definition isn't found, then global definition will be searched for.
348 | 3. AcmeDir takes priority over DocumentRoot, and local definition takes priority over global definition.
349 | 4. The properties (keys/options) are case sensitive.
350 | 5. **True** **False** values are case insensitive.
351 | 6. If the challenge directory (AcmeDir or DocumentRoot) for non-www site isn't defined, then a definition for it's www version will be searched for and vice versa. If both are defined, they are taken as is.
352 |
353 | A full fledged JSON configuration file:
354 | ```json
355 | {
356 | "example.com": {
357 | "DocumentRoot":"/var/www/public_html",
358 | "_comment":"Global defintion AcmeDir won't be used as DocumentRoot is defined"
359 | },
360 | "subdomain1.example.com": {
361 | "DocumentRoot":"/var/www/subdomain1",
362 | "_comment":"Local defintion of DocumentRoot"
363 | },
364 | "www.subdomain2.example.com": {
365 | "AcmeDir":"/var/www/subdomain2",
366 | "_comment":"Local defintion of AcmeDir"
367 | },
368 | "subdomain3.example.com": {
369 | "AcmeDir":"/var/www/subdomain3"
370 | },
371 | "subdomain4.example.com": {
372 | "AcmeDir":"/var/www/subdomain4"
373 | },
374 | "www.subdomain5.example.com": {
375 | "AcmeDir":"/var/www/subdomain5"
376 | },
377 | "AccountKey":"account.key",
378 | "CSR": "domain.csr",
379 | "AcmeDir":"/var/www/html",
380 | "DocumentRoot":"/var/www/public_html",
381 | "_comment":"Global definition of DocumentRoot and AcmeDir",
382 | "CertFile":"domain.crt",
383 | "ChainFile":"chain.crt",
384 | "CA":"",
385 | "__comment":"For CA default value will be used. please don't change this option. Use the Test property if you want the staging api.",
386 | "NoChain":"False",
387 | "NoCert":"False",
388 | "Test":"False",
389 | "Force":"False",
390 | "Quiet":"False"
391 | }
392 | ```
393 | Another full-fledged JSON file using only a single AcmeDir as challenge directory for every domain:
394 |
395 | ```json
396 | {
397 | "AcmeDir":"/var/www/challenge",
398 | "_comment":"Global definition of AcmeDir",
399 | "AccountKey":"account.key",
400 | "CSR": "domain.csr",
401 | "CertFile":"domain.crt",
402 | "ChainFile":"chain.crt",
403 | "CA":"",
404 | "__comment":"For CA default value will be used. please don't change this option. Use the Test property if you want the staging api.",
405 | "NoChain":"False",
406 | "NoCert":"False",
407 | "Test":"False",
408 | "Force":"False",
409 | "Quiet":"False"
410 | }
411 | ```
412 | The above JSON is exactly for the scenario where you are using it in acme-tiny compatible mode or using the 3.3 workaround for shared servers.
413 |
414 | For 3.3 workaround, change the AcmeDir to `/var/www/challenge/challenge` where `/var/www/challenge` is the document root of your dedicated http site (challenge.example.com); in that way you can use the exact redirection code mentioned in section 3.3.
415 |
416 |
417 | # Test suit:
418 | The **test** directory contains a simple Bash script named **check.sh**. It creates two temporary local sites (in */tmp*) and exposes them publicly (careful) on Internet using ngrok, then creates *account key*, *dom.key*, *CSR* for these two sites and gets the certificate and chain. The certificates are retrieved twice: first, by using Document Root and second, by using Acme Dir.
419 |
420 | Part of this script requires root privilege (It needs to create custom local sites and configure/restart apache2).
421 |
422 | This script depends on various other scripts/tools. An **inst.sh** file is provided to install/download the dependencies.
423 |
424 | **Dependencies:**
425 |
426 | 1. Debian based OS
427 | 2. Apache2 server
428 | 3. jq
429 | 4. ngrok
430 | 5. [gencsr](https://github.com/neurobin/gencsr) \[included\]
431 | 5. [lampi](https://github.com/neurobin/lampi) \[included\] (This script creates the local sites)
432 |
433 | You can get the dependencies by running the *inst.sh* script:
434 |
435 | ```sh
436 | chmod +x ./test/inst.sh
437 | ./test/inst.sh
438 | ```
439 | If you already have Apache2 server installed, then just install `jq`. Then download and unzip [ngrok](wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip -O ngrok-stable-linux-amd64.zip) (see *inst.sh* file) in the *test* directory.
440 |
441 | After getting the dependencies, you can run **check.sh** to perform the test:
442 |
443 | ```sh
444 | chmod +x ./test/check.sh
445 | ./test/check.sh
446 | ```
447 |
448 | **Do not run the ./test/travis_check.sh on your local machine.** It's written for [travis build](https://travis-ci.org/neurobin/letsacme) only and contains
449 | unguarded code that can harm your system.
450 |
451 | If you don't want to perform the test yourself but just want to see the outcome, then visit [travis build page for letsacme](https://travis-ci.org/neurobin/letsacme). Travis test uses apache2 Alias in AcmeDir method while the local test uses redirect through .htaccess (the 3.3 workaround).
452 |
453 | **Both tests perform:**
454 |
455 | 1. A test defining document roots in config json.
456 | 2. A test using acme-dir without config json.
457 |
--------------------------------------------------------------------------------