chiark / gitweb /
Keep consistency in config defaults and writes
[fdroidserver.git] / fdroidserver / init.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # update.py - part of the FDroid server tools
5 # Copyright (C) 2010-2013, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
7 # Copyright (C) 2013 Hans-Christoph Steiner <hans@eds.org>
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Affero General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU Affero General Public License for more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22 import glob
23 import hashlib
24 import os
25 import re
26 import shutil
27 import socket
28 import sys
29 from optparse import OptionParser
30 import logging
31
32 import common
33 from common import FDroidPopen, BuildException
34
35 config = {}
36 options = None
37
38
39 def write_to_config(config, key, value=None):
40     '''write a key/value to the local config.py'''
41     if value is None:
42         origkey = key + '_orig'
43         value = config[origkey] if origkey in config else config[key]
44     with open('config.py', 'r') as f:
45         data = f.read()
46     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
47     repl = '\n' + key + ' = "' + value + '"'
48     data = re.sub(pattern, repl, data)
49     with open('config.py', 'w') as f:
50         f.writelines(data)
51
52
53 def disable_in_config(key, value):
54     '''write a key/value to the local config.py, then comment it out'''
55     with open('config.py', 'r') as f:
56         data = f.read()
57     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
58     repl = '\n#' + key + ' = "' + value + '"'
59     data = re.sub(pattern, repl, data)
60     with open('config.py', 'w') as f:
61         f.writelines(data)
62
63
64 def genpassword():
65     '''generate a random password for when generating keys'''
66     h = hashlib.sha256()
67     h.update(os.urandom(16))  # salt
68     h.update(bytes(socket.getfqdn()))
69     return h.digest().encode('base64').strip()
70
71
72 def genkey(keystore, repo_keyalias, password, keydname):
73     '''generate a new keystore with a new key in it for signing repos'''
74     logging.info('Generating a new key in "' + keystore + '"...')
75     common.write_password_file("keystorepass", password)
76     common.write_password_file("keypass", password)
77     p = FDroidPopen(['keytool', '-genkey',
78                      '-keystore', keystore, '-alias', repo_keyalias,
79                      '-keyalg', 'RSA', '-keysize', '4096',
80                      '-sigalg', 'SHA256withRSA',
81                      '-validity', '10000',
82                      '-storepass:file', config['keystorepassfile'],
83                      '-keypass:file', config['keypassfile'],
84                      '-dname', keydname])
85     # TODO keypass should be sent via stdin
86     if p.returncode != 0:
87         raise BuildException("Failed to generate key", p.output)
88     # now show the lovely key that was just generated
89     p = FDroidPopen(['keytool', '-list', '-v',
90                      '-keystore', keystore, '-alias', repo_keyalias,
91                      '-storepass:file', config['keystorepassfile']])
92     logging.info(p.output.strip() + '\n\n')
93
94
95 def main():
96
97     global options, config
98
99     # Parse command line...
100     parser = OptionParser()
101     parser.add_option("-v", "--verbose", action="store_true", default=False,
102                       help="Spew out even more information than normal")
103     parser.add_option("-q", "--quiet", action="store_true", default=False,
104                       help="Restrict output to warnings and errors")
105     parser.add_option("-d", "--distinguished-name", default=None,
106                       help="X.509 'Distiguished Name' used when generating keys")
107     parser.add_option("--keystore", default=None,
108                       help="Path to the keystore for the repo signing key")
109     parser.add_option("--repo-keyalias", default=None,
110                       help="Alias of the repo signing key in the keystore")
111     parser.add_option("--android-home", default=None,
112                       help="Path to the Android SDK (sometimes set in ANDROID_HOME)")
113     parser.add_option("--no-prompt", action="store_true", default=False,
114                       help="Do not prompt for Android SDK path, just fail")
115     (options, args) = parser.parse_args()
116
117     # find root install prefix
118     tmp = os.path.dirname(sys.argv[0])
119     if os.path.basename(tmp) == 'bin':
120         prefix = os.path.dirname(tmp)
121         examplesdir = prefix + '/share/doc/fdroidserver/examples'
122     else:
123         # we're running straight out of the git repo
124         prefix = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
125         examplesdir = prefix + '/examples'
126
127     fdroiddir = os.getcwd()
128     test_config = dict()
129     common.fill_config_defaults(test_config)
130
131     # track down where the Android SDK is, the default is to use the path set
132     # in ANDROID_HOME if that exists, otherwise None
133     if options.android_home is not None:
134         test_config['sdk_path'] = options.android_home
135     elif not common.test_sdk_exists(test_config):
136         # if neither --android-home nor the default sdk_path exist, prompt the user
137         default_sdk_path = '/opt/android-sdk'
138         while not options.no_prompt:
139             try:
140                 s = raw_input('Enter the path to the Android SDK ('
141                               + default_sdk_path + ') here:\n> ')
142             except KeyboardInterrupt:
143                 print('')
144                 sys.exit(1)
145             if re.match('^\s*$', s) is not None:
146                 test_config['sdk_path'] = default_sdk_path
147             else:
148                 test_config['sdk_path'] = s
149             if common.test_sdk_exists(test_config):
150                 break
151     if not common.test_sdk_exists(test_config):
152         sys.exit(3)
153
154     if not os.path.exists('config.py'):
155         # 'metadata' and 'tmp' are created in fdroid
156         if not os.path.exists('repo'):
157             os.mkdir('repo')
158         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
159         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
160         os.chmod('config.py', 0o0600)
161         write_to_config(test_config, 'sdk_path')
162     else:
163         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
164         logging.info('Try running `fdroid init` in an empty directory.')
165         sys.exit()
166
167     # try to find a working aapt, in all the recent possible paths
168     build_tools = os.path.join(test_config['sdk_path'], 'build-tools')
169     aaptdirs = []
170     aaptdirs.append(os.path.join(build_tools, test_config['build_tools']))
171     aaptdirs.append(build_tools)
172     for f in os.listdir(build_tools):
173         if os.path.isdir(os.path.join(build_tools, f)):
174             aaptdirs.append(os.path.join(build_tools, f))
175     for d in sorted(aaptdirs, reverse=True):
176         if os.path.isfile(os.path.join(d, 'aapt')):
177             aapt = os.path.join(d, 'aapt')
178             break
179     if os.path.isfile(aapt):
180         dirname = os.path.basename(os.path.dirname(aapt))
181         if dirname == 'build-tools':
182             # this is the old layout, before versioned build-tools
183             test_config['build_tools'] = ''
184         else:
185             test_config['build_tools'] = dirname
186         write_to_config(test_config, 'build_tools')
187     if not common.test_build_tools_exists(test_config):
188         sys.exit(3)
189
190     # now that we have a local config.py, read configuration...
191     config = common.read_config(options)
192
193     # track down where the Android NDK is
194     ndk_path = '/opt/android-ndk'
195     if os.path.isdir(config['ndk_path']):
196         ndk_path = config['ndk_path']
197     elif 'ANDROID_NDK' in os.environ.keys():
198         logging.info('using ANDROID_NDK')
199         ndk_path = os.environ['ANDROID_NDK']
200     if os.path.isdir(ndk_path):
201         write_to_config(test_config, 'ndk_path')
202     # the NDK is optional so we don't prompt the user for it if its not found
203
204     # find or generate the keystore for the repo signing key. First try the
205     # path written in the default config.py.  Then check if the user has
206     # specified a path from the command line, which will trump all others.
207     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.  If
208     # keystore is set to NONE, that means that Java will look for keys in a
209     # Hardware Security Module aka Smartcard.
210     keystore = config['keystore']
211     if options.keystore:
212         keystore = os.path.abspath(options.keystore)
213         if options.keystore == 'NONE':
214             keystore = options.keystore
215         else:
216             keystore = os.path.abspath(options.keystore)
217             if not os.path.exists(keystore):
218                 logging.info('"' + keystore
219                              + '" does not exist, creating a new keystore there.')
220     write_to_config(test_config, 'keystore', keystore)
221     repo_keyalias = None
222     if options.repo_keyalias:
223         repo_keyalias = options.repo_keyalias
224         write_to_config(test_config, 'repo_keyalias', repo_keyalias)
225     if options.distinguished_name:
226         keydname = options.distinguished_name
227         write_to_config(test_config, 'keydname', keydname)
228     if keystore == 'NONE':  # we're using a smartcard
229         write_to_config(test_config, 'repo_keyalias', '1')  # seems to be the default
230         disable_in_config('keypass', 'never used with smartcard')
231         write_to_config(test_config, 'smartcardoptions',
232                         ('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
233                          + '-providerClass sun.security.pkcs11.SunPKCS11 '
234                          + '-providerArg opensc-fdroid.cfg'))
235         # find opensc-pkcs11.so
236         if not os.path.exists('opensc-fdroid.cfg'):
237             if os.path.exists('/usr/lib/opensc-pkcs11.so'):
238                 opensc_so = '/usr/lib/opensc-pkcs11.so'
239             elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
240                 opensc_so = '/usr/lib64/opensc-pkcs11.so'
241             else:
242                 files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
243                 if len(files) > 0:
244                     opensc_so = files[0]
245                 else:
246                     opensc_so = '/usr/lib/opensc-pkcs11.so'
247                     logging.warn('No OpenSC PKCS#11 module found, ' +
248                                  'install OpenSC then edit "opensc-fdroid.cfg"!')
249             with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
250                 opensc_fdroid = f.read()
251             opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
252                                    flags=re.MULTILINE)
253             with open('opensc-fdroid.cfg', 'w') as f:
254                 f.write(opensc_fdroid)
255     elif not os.path.exists(keystore):
256         # no existing or specified keystore, generate the whole thing
257         keystoredir = os.path.dirname(keystore)
258         if not os.path.exists(keystoredir):
259             os.makedirs(keystoredir, mode=0o700)
260         password = genpassword()
261         write_to_config(test_config, 'keystorepass', password)
262         write_to_config(test_config, 'keypass', password)
263         if options.repo_keyalias is None:
264             repo_keyalias = socket.getfqdn()
265             write_to_config(test_config, 'repo_keyalias', repo_keyalias)
266         if not options.distinguished_name:
267             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
268             write_to_config(test_config, 'keydname', keydname)
269         genkey(keystore, repo_keyalias, password, keydname)
270
271     logging.info('Built repo based in "' + fdroiddir + '"')
272     logging.info('with this config:')
273     logging.info('  Android SDK:\t\t\t' + config['sdk_path'])
274     logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
275     logging.info('  Android NDK (optional):\t' + ndk_path)
276     logging.info('  Keystore for signing key:\t' + keystore)
277     if repo_keyalias is not None:
278         logging.info('  Alias for key in store:\t' + repo_keyalias)
279     logging.info('\nTo complete the setup, add your APKs to "' +
280                  os.path.join(fdroiddir, 'repo') + '"' + '''
281 then run "fdroid update -c; fdroid update".  You might also want to edit
282 "config.py" to set the URL, repo name, and more.  You should also set up
283 a signing key (a temporary one might have been automatically generated).
284
285 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
286 and https://f-droid.org/manual/fdroid.html#Signing
287 ''')