1 /**
2 	Copyright: © 2013 rejectedsoftware e.K.
3 	License: Subject to the terms of the GNU GPLv3 license, as written in the included LICENSE.txt file.
4 	Authors: Sönke Ludwig
5 */
6 module app;
7 
8 import dubregistry.dbcontroller;
9 import dubregistry.mirror;
10 import dubregistry.repositories.bitbucket;
11 import dubregistry.repositories.github;
12 import dubregistry.repositories.gitlab;
13 import dubregistry.registry;
14 import dubregistry.web;
15 import dubregistry.api;
16 
17 import std.algorithm : sort;
18 import std.process : environment;
19 import std.file;
20 import std.path;
21 import std.traits : isIntegral;
22 import userman.db.controller : UserManController, createUserManController;
23 import userman.userman : UserManSettings;
24 import userman.web;
25 import vibe.d;
26 
27 
28 Task s_checkTask;
29 DubRegistry s_registry;
30 DubRegistryWebFrontend s_web;
31 string s_mirror;
32 
33 void checkForNewVersions()
34 {
35 	if (s_mirror.length) s_registry.mirrorRegistry(s_mirror);
36 	else s_registry.updatePackages();
37 }
38 
39 void startMonitoring()
40 {
41 	void monitorPackages()
42 	{
43 		sleep(1.seconds()); // give the cache a chance to warm up first
44 		while(true){
45 			checkForNewVersions;
46 			sleep(30.minutes());
47 		}
48 	}
49 	s_checkTask = runTask(&monitorPackages);
50 }
51 
52 version (linux) private immutable string certPath;
53 
54 shared static this()
55 {
56 	enum debianCA = "/etc/ssl/certs/ca-certificates.crt";
57 	enum redhatCA = "/etc/pki/tls/certs/ca-bundle.crt";
58 	certPath = redhatCA.exists ? redhatCA : debianCA;
59 }
60 
61 // generate dummy data for e.g. Heroku's preview apps
62 void defaultInit(UserManController userMan, DubRegistry registry)
63 {
64 	if (environment.get("GENERATE_DEFAULT_DATA", "0") == "1" &&
65 		registry.getPackageDump().empty && userMan.getUserCount() == 0)
66 	{
67 		logInfo("'GENERATE_DEFAULT_DATA' is set and an empty database has been detected. Inserting dummy data.");
68 		auto userId = userMan.registerUser("dummy@dummy.org", "dummyuser",
69 			"Dummy User", "test1234");
70 		auto packages = [
71 			"https://github.com/libmir/mir-algorithm",
72 			"https://github.com/libmir/mir-runtime",
73 			"https://github.com/libmir/mir-random",
74 			"https://github.com/libmir/mir-core",
75 			"https://gitlab.com/WebFreak001/bancho-irc",
76 		];
77 		foreach (url; packages)
78 		{
79 			DbRepository repo;
80 			repo.parseURL(URL(url));
81 			registry.addPackage(repo, userId);
82 		}
83 	}
84 }
85 
86 struct AppConfig
87 {
88 	string ghuser, ghpassword,
89 		   glurl, glauth,
90 		   bbuser, bbpassword;
91 
92 	bool enforceCertificateTrust = false;
93 
94 	string serviceName = "DUB - The D package registry";
95 	string serviceURL = "https://code.dlang.org/";
96 	string serviceEmail = "noreply@rejectedsoftware.com";
97 
98 	string mailServer;
99 	ushort mailServerPort;
100 	SMTPConnectionType mailConnectionType;
101 	string mailClientName;
102 	string mailUser;
103 	string mailPassword;
104 
105 
106 	void init()
107 	{
108 		import dub.internal.utils : jsonFromFile;
109 		auto regsettingsjson = jsonFromFile(NativePath("settings.json"), true);
110 		// TODO: use UDAs instead
111 		static immutable variables = [
112 			["ghuser", "github-user"],
113 			["ghpassword", "github-password"],
114 			["glurl", "gitlab-url"],
115 			["glauth", "gitlab-auth"],
116 			["bbuser", "bitbucket-user"],
117 			["bbpassword", "bitbucket-password"],
118 			["enforceCertificateTrust", "enforce-certificate-trust"],
119 			["serviceName", "service-name"],
120 			["serviceURL", "service-url"],
121 			["serviceEmail", "service-email"],
122 			["mailServer", "mail-server"],
123 			["mailServerPort", "mail-server-port"],
124 			["mailConnectionType", "mail-connection-type"],
125 			["mailClientName", "mail-client-name"],
126 			["mailUser", "mail-user"],
127 			["mailPassword", "mail-password"],
128 		];
129 		static foreach (var; variables) {{
130 			alias T = typeof(__traits(getMember, this, var[0]));
131 
132 			T val = __traits(getMember, this, var[0]);
133 
134 			if (var[1] in regsettingsjson) {
135 				static if (is(T == bool)) val = regsettingsjson[var[1]].get!bool;
136 				else static if (isIntegral!T && !is(T == enum)) val = regsettingsjson[var[1]].get!int.to!T;
137 				else val = regsettingsjson[var[1]].get!string.to!T;
138 			} else {
139 				// fallback to environment variables
140 				auto ev = environment.get(var[1].replace("-", "_").toUpper);
141 				if (ev.length) val = ev.to!T;
142 			}
143 
144 			__traits(getMember, this, var[0]) = val;
145 		}}
146 	}
147 }
148 
149 void main()
150 {
151 	bool noMonitoring;
152 	setLogFile("log.txt", LogLevel.diagnostic);
153 
154 	version (linux) version (DMD)
155 	{
156 		// register memory error handler on heroku
157 		if ("DYNO" in environment)
158 		{
159 			import etc.linux.memoryerror : registerMemoryErrorHandler;
160 			registerMemoryErrorHandler();
161 		}
162 	}
163 
164 	string hostname = "code.dlang.org";
165 
166 	readOption("mirror", &s_mirror, "URL of a package registry that this instance should mirror (WARNING: will overwrite local database!)");
167 	readOption("hostname", &hostname, "Domain name of this instance (default: code.dlang.org)");
168 	readOption("no-monitoring", &noMonitoring, "Don't periodically monitor for updates (for local development)");
169 
170 	// validate provided mirror URL
171 	if (s_mirror.length)
172 		validateMirrorURL(s_mirror);
173 
174 	AppConfig appConfig;
175 	appConfig.init();
176 
177 	version (linux) {
178 		if (appConfig.enforceCertificateTrust) {
179 			logInfo("Enforcing certificate trust.");
180 			HTTPClient.setTLSSetupCallback((ctx) {
181 				ctx.useTrustedCertificateFile(certPath);
182 				ctx.peerValidationMode = TLSPeerValidationMode.trustedCert;
183 			});
184 		}
185 	}
186 
187 	GithubRepository.register(appConfig.ghuser, appConfig.ghpassword);
188 	BitbucketRepository.register(appConfig.bbuser, appConfig.bbpassword);
189 	if (appConfig.glurl.length) GitLabRepository.register(appConfig.glauth, appConfig.glurl);
190 
191 	auto router = new URLRouter;
192 	if (s_mirror.length) router.any("*", (req, res) { req.params["mirror"] = s_mirror; });
193 	if (!noMonitoring)
194 		router.get("*", (req, res) @trusted { if (!s_checkTask.running) startMonitoring(); });
195 
196 	// init mongo
197 	import dubregistry.mongodb : databaseName, mongoSettings;
198 	mongoSettings();
199 
200 	// VPM registry
201 	auto regsettings = new DubRegistrySettings;
202 	regsettings.databaseName = databaseName;
203 	s_registry = new DubRegistry(regsettings);
204 
205 	UserManController userdb;
206 
207 	if (!s_mirror.length) {
208 		// user management
209 		auto udbsettings = new UserManSettings;
210 		udbsettings.serviceName = appConfig.serviceName;
211 		udbsettings.serviceURL = URL(appConfig.serviceURL);
212 		udbsettings.serviceEmail = appConfig.serviceEmail;
213 		udbsettings.databaseURL = environment.get("MONGODB_URI", environment.get("MONGO_URI", "mongodb://127.0.0.1:27017/vpmreg"));
214 		udbsettings.requireActivation = false;
215 
216 		udbsettings.mailSettings.host = appConfig.mailServer;
217 		udbsettings.mailSettings.port = appConfig.mailServerPort;
218 		udbsettings.mailSettings.connectionType = appConfig.mailConnectionType;
219 		udbsettings.mailSettings.localname = appConfig.mailClientName;
220 		udbsettings.mailSettings.username = appConfig.mailUser;
221 		udbsettings.mailSettings.password = appConfig.mailPassword;
222 		if (appConfig.mailUser.length || appConfig.mailPassword.length)
223 			udbsettings.mailSettings.authType = SMTPAuthType.plain;
224 		udbsettings.mailSettings.tlsValidationMode = TLSPeerValidationMode.validCert;
225 		version (linux) {
226 			if (appConfig.enforceCertificateTrust) {
227 				udbsettings.mailSettings.tlsValidationMode = TLSPeerValidationMode.trustedCert;
228 				udbsettings.mailSettings.tlsContextSetup = (ctx) {
229 					ctx.useTrustedCertificateFile(certPath);
230 				};
231 			}
232 		}
233 
234 		userdb = createUserManController(udbsettings);
235 	}
236 
237 	// web front end
238 	s_web = router.registerDubRegistryWebFrontend(s_registry, userdb);
239 	router.registerDubRegistryAPI(s_registry);
240 
241 	// check whether dummy data should be loaded
242 	defaultInit(userdb, s_registry);
243 
244 	// start the web server
245 	auto settings = new HTTPServerSettings;
246 	settings.hostName = hostname;
247 	settings.bindAddresses = ["127.0.0.1"];
248 	settings.port = 8005;
249 	settings.sessionStore = new MemorySessionStore;
250 	settings.useCompressionIfPossible = true;
251 	readOption("bind", &settings.bindAddresses[0], "Sets the address used for serving.");
252 	readOption("port|p", &settings.port, "Sets the port used for serving.");
253 
254 	listenHTTP(settings, router);
255 
256 	// poll github for new project versions
257 	if (!noMonitoring)
258 		startMonitoring();
259 	runApplication();
260 }