chiark / gitweb /
add options to 'fdroid init': --distinguished-name and --repo-keyalias
[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 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 hashlib
23 import os
24 import re
25 import shutil
26 import socket
27 import subprocess
28 import sys
29 from optparse import OptionParser
30
31 import common
32
33
34 config = {}
35 options = None
36
37 def write_to_config(key, value):
38     '''write a key/value to the local config.py'''
39     with open('config.py', 'r') as f:
40         data = f.read()
41     pattern = key + '\s*=.*'
42     repl = key + ' = "' + value + '"'
43     data = re.sub(pattern, repl, data)
44     with open('config.py', 'w') as f:
45         f.writelines(data)
46
47
48 def genpassword():
49     '''generate a random password for when generating keys'''
50     h = hashlib.sha256()
51     h.update(os.urandom(16)) # salt
52     h.update(bytes(socket.getfqdn()))
53     return h.digest().encode('base64').strip()
54
55
56 def genkey(keystore, repo_keyalias, password, keydname):
57     '''generate a new keystore with a new key in it for signing repos'''
58     print('Generating a new key in "' + keystore + '"...')
59     p = subprocess.Popen(['keytool', '-genkey',
60                           '-keystore', keystore, '-alias', repo_keyalias,
61                           '-keyalg', 'RSA', '-keysize', '4096',
62                           '-sigalg', 'SHA256withRSA',
63                           '-validity', '10000',
64                           '-storepass', password, '-keypass', password,
65                           '-dname', keydname],
66                          stdout=subprocess.PIPE)
67     output = p.communicate()[0]
68     print(output)
69     if p.returncode != 0:
70         raise BuildException("Failed to generate key")
71     # now show the lovely key that was just generated
72     p = subprocess.Popen(['keytool', '-list', '-v',
73                           '-keystore', keystore, '-alias', repo_keyalias],
74                          stdin=subprocess.PIPE,
75                          stdout=subprocess.PIPE,
76                          stderr=subprocess.PIPE)
77     output = p.communicate(password)[0]
78     print(output.lstrip().strip() + '\n\n')
79
80
81 def main():
82
83     global options, config
84
85     # Parse command line...
86     parser = OptionParser()
87     parser.add_option("-v", "--verbose", action="store_true", default=False,
88                       help="Spew out even more information than normal")
89     parser.add_option("-d", "--distinguished-name", default=None,
90                       help="X.509 'Distiguished Name' used when generating keys")
91     parser.add_option("--keystore", default=None,
92                       help="Path to the keystore for the repo signing key")
93     parser.add_option("--repo-keyalias", default=None,
94                       help="Alias of the repo signing key in the keystore")
95     (options, args) = parser.parse_args()
96
97     # find root install prefix
98     tmp = os.path.dirname(sys.argv[0])
99     if os.path.basename(tmp) == 'bin':
100         prefix = os.path.dirname(tmp)
101         examplesdir = prefix + '/share/doc/fdroidserver/examples'
102     else:
103         # we're running straight out of the git repo
104         prefix = tmp
105         examplesdir = prefix
106
107     repodir = os.getcwd()
108
109     if not os.path.exists('config.py') and not os.path.exists('repo'):
110         # 'metadata' and 'tmp' are created in fdroid
111         os.mkdir('repo')
112         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), repodir)
113         shutil.copyfile(os.path.join(examplesdir, 'config.sample.py'), 'config.py')
114     else:
115         print('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
116         sys.exit()
117
118     # now that we have a local config.py, read configuration...
119     config = common.read_config(options)
120
121     # track down where the Android SDK is
122     if os.path.isdir(config['sdk_path']):
123         print('Using "' + config['sdk_path'] + '" for the Android SDK')
124         sdk_path = config['sdk_path']
125     elif 'ANDROID_HOME' in os.environ.keys():
126         sdk_path = os.environ['ANDROID_HOME']
127     else:
128         default_sdk_path = '/opt/android-sdk'
129         while True:
130             s = raw_input('Enter the path to the Android SDK (' + default_sdk_path + '): ')
131             if re.match('^\s*$', s) != None:
132                 sdk_path = default_sdk_path
133             else:
134                 sdk_path = s
135             if os.path.isdir(os.path.join(sdk_path, 'build-tools')):
136                 break
137             else:
138                 print('"' + s + '" does not contain the Android SDK! Try again...')
139     if os.path.isdir(sdk_path):
140         write_to_config('sdk_path', sdk_path)
141
142     # try to find a working aapt, in all the recent possible paths
143     build_tools = os.path.join(sdk_path, 'build-tools')
144     aaptdirs = []
145     aaptdirs.append(os.path.join(build_tools, config['build_tools']))
146     aaptdirs.append(build_tools)
147     for f in sorted(os.listdir(build_tools), reverse=True):
148         if os.path.isdir(os.path.join(build_tools, f)):
149             aaptdirs.append(os.path.join(build_tools, f))
150     for d in aaptdirs:
151         if os.path.isfile(os.path.join(d, 'aapt')):
152             aapt = os.path.join(d, 'aapt')
153             break
154     if os.path.isfile(aapt):
155         dirname = os.path.basename(os.path.dirname(aapt))
156         if dirname == 'build-tools':
157             # this is the old layout, before versioned build-tools
158             write_to_config('build_tools', '')
159         else:
160             write_to_config('build_tools', dirname)
161
162     # track down where the Android NDK is
163     ndk_path = '/opt/android-ndk'
164     if os.path.isdir(config['ndk_path']):
165         ndk_path = config['ndk_path']
166     elif 'ANDROID_NDK' in os.environ.keys():
167         print('using ANDROID_NDK')
168         ndk_path = os.environ['ANDROID_NDK']
169     if os.path.isdir(ndk_path):
170         write_to_config('ndk_path', ndk_path)
171     # the NDK is optional so we don't prompt the user for it if its not found
172
173     # find or generate the keystore for the repo signing key. First try the
174     # path written in the default config.py.  Then check if the user has
175     # specified a path from the command line, which will trump all others.
176     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.
177     keystore = config['keystore']
178     if options.keystore:
179         if os.path.isfile(options.keystore):
180             keystore = options.keystore
181             write_to_config('keystore', keystore)
182         else:
183             print('"' + options.keystore + '" does not exist or is not a file!')
184             sys.exit(1)
185     if options.repo_keyalias:
186         repo_keyalias = options.repo_keyalias
187         write_to_config('repo_keyalias', repo_keyalias)
188     if options.distinguished_name:
189         keydname = options.distinguished_name
190         write_to_config('keydname', keydname)
191     if not os.path.isfile(keystore):
192         # no existing or specified keystore, generate the whole thing
193         keystoredir = os.path.join(os.getenv('HOME'),
194                                    '.local', 'share', 'fdroidserver')
195         if not os.path.exists(keystoredir):
196             os.makedirs(keystoredir, mode=0o700)
197         keystore = os.path.join(keystoredir, 'keystore.jks')
198         write_to_config('keystore', keystore)
199         password = genpassword()
200         write_to_config('keystorepass', password)
201         write_to_config('keypass', password)
202         if not options.repo_keyalias:
203             repo_keyalias = socket.getfqdn()
204             write_to_config('repo_keyalias', repo_keyalias)
205         if not options.distinguished_name:
206             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
207             write_to_config('keydname', keydname)
208         genkey(keystore, repo_keyalias, password, keydname)
209
210     print('Built repo in "' + repodir + '" with this config:')
211     print('  Android SDK:\t\t\t' + sdk_path)
212     print('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
213     print('  Android NDK (optional):\t' + ndk_path)
214     print('  Keystore for signing key:\t' + keystore)
215     print('\nTo complete the setup, add your APKs to "' +
216           os.path.join(repodir, 'repo') + '"' +
217 '''
218 then run "fdroid update -c; fdroid update".  You might also want to edit
219 "config.py" to set the URL, repo name, and more.  You should also set up
220 a signing key.
221
222 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
223 and https://f-droid.org/manual/fdroid.html#Signing
224 ''')