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