
config: Fix bugs about handling config files
Yobi did not handle some cases correctly about config files. * Fix a bug that the config files specified by user are ignored; In the case, do not create the files by default even if they don't exist. * Do not update application.secret if the config file is out of conf directory. The file may not be Yobi's. * Tell the user to update application.secret if Yobi failed to update it. * Refactoring: Move members for config files into the ConfigFile inner class
@7caf00dfa63a8188c1f6756194e42bc14504ab41
--- app/Global.java
+++ app/Global.java
... | ... | @@ -19,35 +19,40 @@ |
19 | 19 |
* limitations under the License. |
20 | 20 |
*/ |
21 | 21 |
|
22 |
-import mailbox.MailboxService; |
|
23 | 22 |
import com.avaje.ebean.Ebean; |
23 |
+import com.typesafe.config.ConfigFactory; |
|
24 | 24 |
import controllers.SvnApp; |
25 | 25 |
import controllers.UserApp; |
26 | 26 |
import controllers.routes; |
27 |
+import mailbox.MailboxService; |
|
27 | 28 |
import models.*; |
28 | 29 |
import org.apache.commons.lang3.StringUtils; |
29 | 30 |
import org.apache.http.impl.cookie.DateUtils; |
30 | 31 |
import play.Application; |
32 |
+import play.Configuration; |
|
31 | 33 |
import play.GlobalSettings; |
32 | 34 |
import play.Play; |
33 | 35 |
import play.api.mvc.Handler; |
34 | 36 |
import play.data.Form; |
37 |
+import play.libs.F.Promise; |
|
35 | 38 |
import play.mvc.Action; |
36 | 39 |
import play.mvc.Http; |
37 | 40 |
import play.mvc.Http.RequestHeader; |
38 |
-import play.mvc.Result; |
|
39 |
-import play.libs.F.Promise; |
|
40 |
- |
|
41 | 41 |
import play.mvc.Result; |
42 | 42 |
import play.mvc.Results; |
43 | 43 |
import utils.*; |
44 | 44 |
import views.html.welcome.restart; |
45 | 45 |
import views.html.welcome.secret; |
46 | 46 |
|
47 |
+import javax.annotation.Nonnull; |
|
48 |
+import java.io.File; |
|
47 | 49 |
import java.io.IOException; |
50 |
+import java.io.InputStream; |
|
48 | 51 |
import java.lang.reflect.Method; |
49 | 52 |
import java.math.BigInteger; |
50 | 53 |
import java.net.InetAddress; |
54 |
+import java.net.URI; |
|
55 |
+import java.net.URISyntaxException; |
|
51 | 56 |
import java.nio.file.Files; |
52 | 57 |
import java.nio.file.Path; |
53 | 58 |
import java.nio.file.Paths; |
... | ... | @@ -60,41 +65,70 @@ |
60 | 65 |
|
61 | 66 |
public class Global extends GlobalSettings { |
62 | 67 |
private static final String[] INITIAL_ENTITY_NAME = {"users", "roles", "siteAdmins"}; |
63 |
- public static final String APPLICATION_CONF_DEFAULT = "application.conf.default"; |
|
64 |
- public static final String APPLICATION_CONF = "application.conf"; |
|
65 |
- public static final String CONFIG_DIRNAME = "conf"; |
|
66 | 68 |
private final String DEFAULT_SECRET = "VA2v:_I=h9>?FYOH:@ZhW]01P<mWZAKlQ>kk>Bo`mdCiA>pDw64FcBuZdDh<47Ew"; |
67 | 69 |
|
68 | 70 |
private boolean isSecretInvalid = false; |
69 | 71 |
private boolean isRestartRequired = false; |
70 |
- private boolean isValidationRequired = false; |
|
71 | 72 |
private MailboxService mailboxService = new MailboxService(); |
73 |
+ private boolean hasFailedToUpdateSecretKey = false; |
|
74 |
+ |
|
75 |
+ private ConfigFile configFile = new ConfigFile("config", "application.conf"); |
|
76 |
+ private ConfigFile loggerConfigFile = new ConfigFile("logger", "application-logger.xml"); |
|
72 | 77 |
|
73 | 78 |
@Override |
74 | 79 |
public Configuration onLoadConfig(play.Configuration config, File path, ClassLoader classloader) { |
75 |
- String basePath = path.getAbsolutePath(); |
|
76 |
- Path pathToDefaultConfig = Paths.get(basePath, CONFIG_DIRNAME, APPLICATION_CONF_DEFAULT); |
|
77 |
- Path pathToConfig = Paths.get(basePath, CONFIG_DIRNAME, APPLICATION_CONF); |
|
78 |
- File configFile = pathToConfig.toFile(); |
|
80 |
+ initLoggerConfig(); |
|
81 |
+ return initConfig(classloader); |
|
82 |
+ } |
|
79 | 83 |
|
80 |
- if (!configFile.exists()) { |
|
81 |
- try { |
|
82 |
- Files.copy(pathToDefaultConfig, pathToConfig); |
|
83 |
- } catch (IOException e) { |
|
84 |
- play.Logger.error("Failed to initialize configuration", e); |
|
85 |
- return null; |
|
86 |
- } |
|
87 |
- Config parsedConfig = ConfigFactory.parseFileAnySyntax(configFile); |
|
88 |
- return new Configuration(ConfigFactory.load(classloader, parsedConfig)); |
|
89 |
- } else { |
|
90 |
- if (!configFile.isFile()) { |
|
91 |
- play.Logger.error( |
|
92 |
- "Failed to initialize configuration: " + pathToConfig + " is a directory."); |
|
93 |
- return null; |
|
94 |
- } |
|
84 |
+ /** |
|
85 |
+ * Creates application.xml by default if necessary |
|
86 |
+ * |
|
87 |
+ * @param classloader |
|
88 |
+ * @return the configuration read from the created file, |
|
89 |
+ * or null if this method didn't create the file. |
|
90 |
+ */ |
|
91 |
+ private Configuration initConfig(ClassLoader classloader) { |
|
92 |
+ if (configFile.isLocationSpecified()) { |
|
93 |
+ return null; |
|
95 | 94 |
} |
96 | 95 |
|
97 |
- return null; |
|
96 |
+ try { |
|
97 |
+ if (configFile.getPath().toFile().exists()) { |
|
98 |
+ return null; |
|
99 |
+ } |
|
100 |
+ } catch (URISyntaxException e) { |
|
101 |
+ play.Logger.error("Failed to check whether the config file exists", e); |
|
102 |
+ return null; |
|
103 |
+ } |
|
104 |
+ |
|
105 |
+ try { |
|
106 |
+ configFile.createByDefault(); |
|
107 |
+ return new Configuration(ConfigFactory.load(classloader, |
|
108 |
+ ConfigFactory.parseFileAnySyntax(configFile.getPath().toFile()))); |
|
109 |
+ } catch (Exception e) { |
|
110 |
+ play.Logger.error("Failed to initialize configuration", e); |
|
111 |
+ return null; |
|
112 |
+ } |
|
113 |
+ } |
|
114 |
+ |
|
115 |
+ /** |
|
116 |
+ * Creates application-logger.xml by default if necessary |
|
117 |
+ * |
|
118 |
+ * Note: This method creates application-logger.xml even if logger.xml exists. |
|
119 |
+ */ |
|
120 |
+ private void initLoggerConfig() { |
|
121 |
+ try { |
|
122 |
+ if (!loggerConfigFile.isLocationSpecified() && !loggerConfigFile.getPath().toFile().exists()) { |
|
123 |
+ try { |
|
124 |
+ loggerConfigFile.createByDefault(); |
|
125 |
+ } catch (Exception e) { |
|
126 |
+ play.Logger.error("Failed to initialize logger configuration", e); |
|
127 |
+ } |
|
128 |
+ } |
|
129 |
+ } catch (URISyntaxException e) { |
|
130 |
+ play.Logger.error("Failed to check whether the logger config file exists", e); |
|
131 |
+ } |
|
98 | 132 |
} |
99 | 133 |
|
100 | 134 |
@Override |
... | ... | @@ -159,7 +193,7 @@ |
159 | 193 |
return new Action.Simple() { |
160 | 194 |
@Override |
161 | 195 |
public Promise<Result> call(Http.Context ctx) throws Throwable { |
162 |
- return Promise.pure((Result) ok(restart.render())); |
|
196 |
+ return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey))); |
|
163 | 197 |
} |
164 | 198 |
}; |
165 | 199 |
} |
... | ... | @@ -176,9 +210,14 @@ |
176 | 210 |
} |
177 | 211 |
|
178 | 212 |
User siteAdmin = SiteAdmin.updateDefaultSiteAdmin(newSiteAdminUserForm.get()); |
179 |
- replaceSiteSecretKey(createSeed(siteAdmin.loginId + ":" + siteAdmin.password)); |
|
213 |
+ try { |
|
214 |
+ updateSiteSecretKey(createSeed(siteAdmin.loginId + ":" + siteAdmin.password)); |
|
215 |
+ } catch (Exception e) { |
|
216 |
+ play.Logger.warn("Failed to update secret key", e); |
|
217 |
+ hasFailedToUpdateSecretKey = true; |
|
218 |
+ } |
|
180 | 219 |
isRestartRequired = true; |
181 |
- return Promise.pure((Result) ok(restart.render())); |
|
220 |
+ return Promise.pure((Result) ok(restart.render(hasFailedToUpdateSecretKey))); |
|
182 | 221 |
} else { |
183 | 222 |
return Promise.pure((Result) ok(secret.render(SiteAdmin.SITEADMIN_DEFAULT_LOGINID, new Form<>(User.class)))); |
184 | 223 |
} |
... | ... | @@ -194,15 +233,18 @@ |
194 | 233 |
return seed; |
195 | 234 |
} |
196 | 235 |
|
197 |
- private void replaceSiteSecretKey(String seed) throws IOException { |
|
236 |
+ private void updateSiteSecretKey(String seed) throws Exception { |
|
198 | 237 |
SecureRandom random = new SecureRandom(seed.getBytes(Config.getCharset())); |
199 | 238 |
String secret = new BigInteger(130, random).toString(32); |
200 | 239 |
|
201 |
- Path path = Paths.get("conf/application.conf"); |
|
202 |
- byte[] bytes = Files.readAllBytes(path); |
|
240 |
+ if (configFile.isExternal()) { |
|
241 |
+ throw new Exception("Cowardly refusing to update an external file: " + configFile.getPath()); |
|
242 |
+ } |
|
243 |
+ |
|
244 |
+ byte[] bytes = Files.readAllBytes(configFile.getPath()); |
|
203 | 245 |
String config = new String(bytes, Config.getCharset()); |
204 | 246 |
config = config.replace(DEFAULT_SECRET, secret); |
205 |
- Files.write(path, config.getBytes(Config.getCharset())); |
|
247 |
+ Files.write(configFile.getPath(), config.getBytes(Config.getCharset())); |
|
206 | 248 |
} |
207 | 249 |
|
208 | 250 |
private boolean hasError(Form<User> newUserForm) { |
... | ... | @@ -273,4 +315,74 @@ |
273 | 315 |
return Promise.pure((Result) badRequest(ErrorViews.BadRequest.render())); |
274 | 316 |
} |
275 | 317 |
|
318 |
+ private static class ConfigFile { |
|
319 |
+ private static final String CONFIG_DIRNAME = "conf"; |
|
320 |
+ private final String fileName; |
|
321 |
+ private final String defaultFileName; |
|
322 |
+ private final String propertyGroup; |
|
323 |
+ |
|
324 |
+ ConfigFile(String propertyGroup, String fileName) { |
|
325 |
+ this.propertyGroup = propertyGroup; |
|
326 |
+ this.fileName = fileName; |
|
327 |
+ this.defaultFileName = fileName + ".default"; |
|
328 |
+ } |
|
329 |
+ |
|
330 |
+ String getProperty(@Nonnull String key) { |
|
331 |
+ return System.getProperty(propertyGroup + "." + key); |
|
332 |
+ } |
|
333 |
+ |
|
334 |
+ String getProperty(@Nonnull String key, String defaultValue) { |
|
335 |
+ return System.getProperty(propertyGroup + "." + key, defaultValue); |
|
336 |
+ } |
|
337 |
+ |
|
338 |
+ /** |
|
339 |
+ * The location of the config file is specified by user |
|
340 |
+ */ |
|
341 |
+ boolean isLocationSpecified() { |
|
342 |
+ return (getProperty("resource") != null) |
|
343 |
+ || (getProperty("file") != null) |
|
344 |
+ || (getProperty("url") != null); |
|
345 |
+ } |
|
346 |
+ |
|
347 |
+ void createByDefault() throws IOException, URISyntaxException { |
|
348 |
+ InputStream stream = Config.class.getClassLoader().getResourceAsStream(defaultFileName); |
|
349 |
+ |
|
350 |
+ getPath().toFile().getParentFile().mkdirs(); |
|
351 |
+ |
|
352 |
+ if (stream != null) { |
|
353 |
+ Files.copy(stream, getPath()); |
|
354 |
+ } else { |
|
355 |
+ Files.copy(getDirectoryPath().resolve(defaultFileName), getPath()); |
|
356 |
+ } |
|
357 |
+ } |
|
358 |
+ |
|
359 |
+ /** |
|
360 |
+ * @return the path to the configuration file |
|
361 |
+ * @throws java.lang.IllegalStateException |
|
362 |
+ */ |
|
363 |
+ Path getPath() throws URISyntaxException { |
|
364 |
+ if (getProperty("url") != null) { |
|
365 |
+ return Paths.get(new URI(getProperty("url"))); |
|
366 |
+ } |
|
367 |
+ |
|
368 |
+ if (getProperty("file") != null) { |
|
369 |
+ return Paths.get(getProperty("file")); |
|
370 |
+ } |
|
371 |
+ |
|
372 |
+ String filename = getProperty("resource", fileName); |
|
373 |
+ |
|
374 |
+ return getDirectoryPath().resolve(filename); |
|
375 |
+ } |
|
376 |
+ |
|
377 |
+ /** |
|
378 |
+ * @return the path to the directory to store configuration files |
|
379 |
+ */ |
|
380 |
+ static Path getDirectoryPath() { |
|
381 |
+ return Paths.get(CONFIG_DIRNAME); |
|
382 |
+ } |
|
383 |
+ |
|
384 |
+ boolean isExternal() throws IOException, URISyntaxException { |
|
385 |
+ return !FileUtil.isSubpathOf(getPath(), getDirectoryPath()); |
|
386 |
+ } |
|
387 |
+ } |
|
276 | 388 |
} |
--- app/utils/FileUtil.java
+++ app/utils/FileUtil.java
... | ... | @@ -29,6 +29,7 @@ |
29 | 29 |
|
30 | 30 |
import java.io.*; |
31 | 31 |
import java.nio.charset.Charset; |
32 |
+import java.nio.file.Path; |
|
32 | 33 |
|
33 | 34 |
public class FileUtil { |
34 | 35 |
|
... | ... | @@ -145,4 +146,25 @@ |
145 | 146 |
return mediaType.hasParameters() |
146 | 147 |
? mediaType.getParameters().get("charset") : null; |
147 | 148 |
} |
149 |
+ |
|
150 |
+ /** |
|
151 |
+ * Checks whether the subpath is a subpath of the given path |
|
152 |
+ * |
|
153 |
+ * @param subpath |
|
154 |
+ * @param path |
|
155 |
+ * @return true if the subpath is a subpath of the given path |
|
156 |
+ * @throws IOException |
|
157 |
+ */ |
|
158 |
+ public static boolean isSubpathOf(Path subpath, Path path) throws IOException { |
|
159 |
+ return isSubpathOf(subpath, path, true); |
|
160 |
+ } |
|
161 |
+ |
|
162 |
+ public static boolean isSubpathOf(Path subpath, Path path, boolean resolveSymlink) throws IOException { |
|
163 |
+ if (resolveSymlink) { |
|
164 |
+ path = path.toRealPath(); |
|
165 |
+ subpath = subpath.toRealPath(); |
|
166 |
+ } |
|
167 |
+ |
|
168 |
+ return subpath.normalize().startsWith(path.normalize()); |
|
169 |
+ } |
|
148 | 170 |
} |
--- app/views/welcome/restart.scala.html
+++ app/views/welcome/restart.scala.html
... | ... | @@ -18,6 +18,7 @@ |
18 | 18 |
* See the License for the specific language governing permissions and |
19 | 19 |
* limitations under the License. |
20 | 20 |
**@ |
21 |
+@(hasFailedToUpdateSecret: Boolean = false) |
|
21 | 22 |
@import utils.TemplateHelper._ |
22 | 23 |
<!DOCTYPE html> |
23 | 24 |
<html> |
... | ... | @@ -57,6 +58,9 @@ |
57 | 58 |
<h3>@Messages("app.restart.welcome")</h3> |
58 | 59 |
<p class="secret-box txt-center"> |
59 | 60 |
@Html(Messages("app.restart.notice")) |
61 |
+ @if(hasFailedToUpdateSecret) { |
|
62 |
+ @Html(Messages("app.restart.updateSecretYourself")) |
|
63 |
+ } |
|
60 | 64 |
</p> |
61 | 65 |
</div> |
62 | 66 |
</div> |
--- conf/messages
+++ conf/messages
... | ... | @@ -24,6 +24,7 @@ |
24 | 24 |
app.description = Web-based platform for collaborative software development |
25 | 25 |
app.name = Yobi |
26 | 26 |
app.restart.notice = Server needs to be restarted. |
27 |
+app.restart.updateSecretYourself = Please update application.secret with random text. |
|
27 | 28 |
app.restart.welcome = Welcome! |
28 | 29 |
app.welcome = Tada! Welcome to {0}! |
29 | 30 |
app.welcome.project = Project |
--- conf/messages.ko-KR
+++ conf/messages.ko-KR
... | ... | @@ -24,6 +24,7 @@ |
24 | 24 |
app.description = 웹기반 소프트웨어 개발 플랫폼 |
25 | 25 |
app.name = Yobi |
26 | 26 |
app.restart.notice = 서버를 재시작해야합니다. |
27 |
+app.restart.updateSecretYourself = application.secret을 무작위 문자열로 변경해주십시오. |
|
27 | 28 |
app.restart.welcome = 환영합니다! |
28 | 29 |
app.welcome = {0}를 시작합니다! |
29 | 30 |
app.welcome.project = 프로젝트 |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?