James Moger
2015-11-22 ed552ba47c02779c270ffd62841d6d1048dade70
commit | author | age
41124c 1 /*
EM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit;
17
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
20 import java.io.File;
21 import java.io.FileWriter;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.OutputStream;
26 import java.net.InetAddress;
27 import java.net.ServerSocket;
28 import java.net.Socket;
29 import java.net.URI;
30 import java.net.URL;
31 import java.net.UnknownHostException;
32 import java.security.ProtectionDomain;
33 import java.text.MessageFormat;
34 import java.util.ArrayList;
35 import java.util.Date;
36 import java.util.List;
37 import java.util.Properties;
38 import java.util.Scanner;
39
40 import org.apache.log4j.PropertyConfigurator;
41 import org.eclipse.jetty.security.ConstraintMapping;
42 import org.eclipse.jetty.security.ConstraintSecurityHandler;
9ef027 43 import org.eclipse.jetty.server.HttpConfiguration;
JM 44 import org.eclipse.jetty.server.HttpConnectionFactory;
41124c 45 import org.eclipse.jetty.server.Server;
9ef027 46 import org.eclipse.jetty.server.ServerConnector;
41124c 47 import org.eclipse.jetty.server.session.HashSessionManager;
EM 48 import org.eclipse.jetty.util.security.Constraint;
49 import org.eclipse.jetty.util.thread.QueuedThreadPool;
50 import org.eclipse.jetty.webapp.WebAppContext;
51 import org.eclipse.jgit.storage.file.FileBasedConfig;
52 import org.eclipse.jgit.util.FS;
53 import org.eclipse.jgit.util.FileUtils;
54 import org.kohsuke.args4j.CmdLineException;
55 import org.kohsuke.args4j.CmdLineParser;
56 import org.kohsuke.args4j.Option;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import com.gitblit.authority.GitblitAuthority;
61 import com.gitblit.authority.NewCertificateConfig;
62 import com.gitblit.servlet.GitblitContext;
63 import com.gitblit.utils.StringUtils;
64 import com.gitblit.utils.TimeUtils;
65 import com.gitblit.utils.X509Utils;
66 import com.gitblit.utils.X509Utils.X509Log;
67 import com.gitblit.utils.X509Utils.X509Metadata;
68 import com.unboundid.ldap.listener.InMemoryDirectoryServer;
69 import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
70 import com.unboundid.ldap.listener.InMemoryListenerConfig;
71 import com.unboundid.ldif.LDIFReader;
72
73 /**
74  * GitBlitServer is the embedded Jetty server for Gitblit GO. This class starts
75  * and stops an instance of Jetty that is configured from a combination of the
76  * gitblit.properties file and command line parameters. JCommander is used to
77  * simplify command line parameter processing. This class also automatically
78  * generates a self-signed certificate for localhost, if the keystore does not
79  * already exist.
80  *
81  * @author James Moger
82  *
83  */
84 public class GitBlitServer {
85
86     private static Logger logger;
87
88     public static void main(String... args) {
89         GitBlitServer server = new GitBlitServer();
90
91         // filter out the baseFolder parameter
92         List<String> filtered = new ArrayList<String>();
93         String folder = "data";
94         for (int i = 0; i < args.length; i++) {
95             String arg = args[i];
96             if (arg.equals("--baseFolder")) {
97                 if (i + 1 == args.length) {
98                     System.out.println("Invalid --baseFolder parameter!");
99                     System.exit(-1);
100                 } else if (!".".equals(args[i + 1])) {
101                     folder = args[i + 1];
102                 }
103                 i = i + 1;
104             } else {
105                 filtered.add(arg);
106             }
107         }
108
109         Params.baseFolder = folder;
110         Params params = new Params();
111         CmdLineParser parser = new CmdLineParser(params);
112         try {
113             parser.parseArgument(filtered);
114             if (params.help) {
115                 server.usage(parser, null);
116             }
117         } catch (CmdLineException t) {
118             server.usage(parser, t);
119         }
120
121         if (params.stop) {
122             server.stop(params);
123         } else {
124             server.start(params);
125         }
126     }
127
128     /**
129      * Display the command line usage of Gitblit GO.
130      *
131      * @param parser
132      * @param t
133      */
134     protected final void usage(CmdLineParser parser, CmdLineException t) {
135         System.out.println(Constants.BORDER);
136         System.out.println(Constants.getGitBlitVersion());
137         System.out.println(Constants.BORDER);
138         System.out.println();
139         if (t != null) {
140             System.out.println(t.getMessage());
141             System.out.println();
142         }
143         if (parser != null) {
144             parser.printUsage(System.out);
145             System.out
146                     .println("\nExample:\n  java -server -Xmx1024M -jar gitblit.jar --repositoriesFolder c:\\git --httpPort 80 --httpsPort 443");
147         }
148         System.exit(0);
149     }
150
43ddbf 151     protected File getBaseFolder(Params params) {
JM 152         String path = System.getProperty("GITBLIT_HOME", Params.baseFolder);
153         if (!StringUtils.isEmpty(System.getenv("GITBLIT_HOME"))) {
154             path = System.getenv("GITBLIT_HOME");
155         }
156
157         return new File(path).getAbsoluteFile();
158     }
159
41124c 160     /**
EM 161      * Stop Gitblt GO.
162      */
163     public void stop(Params params) {
164         try {
165             Socket s = new Socket(InetAddress.getByName("127.0.0.1"), params.shutdownPort);
166             OutputStream out = s.getOutputStream();
167             System.out.println("Sending Shutdown Request to " + Constants.NAME);
168             out.write("\r\n".getBytes());
169             out.flush();
170             s.close();
171         } catch (UnknownHostException e) {
172             e.printStackTrace();
173         } catch (IOException e) {
174             e.printStackTrace();
175         }
176     }
177
178     /**
179      * Start Gitblit GO.
180      */
181     protected final void start(Params params) {
43ddbf 182         final File baseFolder = getBaseFolder(params);
41124c 183         FileSettings settings = params.FILESETTINGS;
EM 184         if (!StringUtils.isEmpty(params.settingsfile)) {
185             if (new File(params.settingsfile).exists()) {
186                 settings = new FileSettings(params.settingsfile);
187             }
188         }
189
190         if (params.dailyLogFile) {
191             // Configure log4j for daily log file generation
192             InputStream is = null;
193             try {
194                 is = getClass().getResourceAsStream("/log4j.properties");
195                 Properties loggingProperties = new Properties();
196                 loggingProperties.load(is);
197
198                 loggingProperties.put("log4j.appender.R.File", new File(baseFolder, "logs/gitblit.log").getAbsolutePath());
199                 loggingProperties.put("log4j.rootCategory", "INFO, R");
200
201                 if (settings.getBoolean(Keys.web.debugMode, false)) {
202                     loggingProperties.put("log4j.logger.com.gitblit", "DEBUG");
203                 }
204
205                 PropertyConfigurator.configure(loggingProperties);
206             } catch (Exception e) {
207                 e.printStackTrace();
208             } finally {
209                 try {
4f03c7 210                     if (is != null) {
JM 211                         is.close();
212                     }
41124c 213                 } catch (IOException e) {
EM 214                     e.printStackTrace();
215                 }
216             }
217         }
218
219         logger = LoggerFactory.getLogger(GitBlitServer.class);
4f92ba 220         logger.info("\n" + Constants.getASCIIArt());
41124c 221
EM 222         System.setProperty("java.awt.headless", "true");
223
224         String osname = System.getProperty("os.name");
225         String osversion = System.getProperty("os.version");
226         logger.info("Running on " + osname + " (" + osversion + ")");
227
9ef027 228         QueuedThreadPool threadPool = new QueuedThreadPool();
JM 229         int maxThreads = settings.getInteger(Keys.server.threadPoolSize, 50);
230         if (maxThreads > 0) {
231             threadPool.setMaxThreads(maxThreads);
41124c 232         }
9ef027 233
JM 234         Server server = new Server(threadPool);
235         server.setStopAtShutdown(true);
41124c 236
EM 237         // conditionally configure the https connector
238         if (params.securePort > 0) {
239             File certificatesConf = new File(baseFolder, X509Utils.CA_CONFIG);
240             File serverKeyStore = new File(baseFolder, X509Utils.SERVER_KEY_STORE);
241             File serverTrustStore = new File(baseFolder, X509Utils.SERVER_TRUST_STORE);
242             File caRevocationList = new File(baseFolder, X509Utils.CA_REVOCATION_LIST);
243
244             // generate CA & web certificates, create certificate stores
245             X509Metadata metadata = new X509Metadata("localhost", params.storePassword);
246             // set default certificate values from config file
247             if (certificatesConf.exists()) {
248                 FileBasedConfig config = new FileBasedConfig(certificatesConf, FS.detect());
249                 try {
250                     config.load();
251                 } catch (Exception e) {
252                     logger.error("Error parsing " + certificatesConf, e);
253                 }
254                 NewCertificateConfig certificateConfig = NewCertificateConfig.KEY.parse(config);
255                 certificateConfig.update(metadata);
256             }
257
258             metadata.notAfter = new Date(System.currentTimeMillis() + 10*TimeUtils.ONEYEAR);
259             X509Utils.prepareX509Infrastructure(metadata, baseFolder, new X509Log() {
260                 @Override
261                 public void log(String message) {
262                     BufferedWriter writer = null;
263                     try {
264                         writer = new BufferedWriter(new FileWriter(new File(baseFolder, X509Utils.CERTS + File.separator + "log.txt"), true));
265                         writer.write(MessageFormat.format("{0,date,yyyy-MM-dd HH:mm}: {1}", new Date(), message));
266                         writer.newLine();
267                         writer.flush();
268                     } catch (Exception e) {
269                         LoggerFactory.getLogger(GitblitAuthority.class).error("Failed to append log entry!", e);
270                     } finally {
271                         if (writer != null) {
272                             try {
273                                 writer.close();
274                             } catch (IOException e) {
275                             }
276                         }
277                     }
278                 }
279             });
280
281             if (serverKeyStore.exists()) {
9ef027 282                 /*
JM 283                  * HTTPS
284                  */
285                 logger.info("Setting up HTTPS transport on port " + params.securePort);
286                 GitblitSslContextFactory factory = new GitblitSslContextFactory(params.alias,
287                         serverKeyStore, serverTrustStore, params.storePassword, caRevocationList);
288                 if (params.requireClientCertificates) {
289                     factory.setNeedClientAuth(true);
290                 } else {
291                     factory.setWantClientAuth(true);
292                 }
293
294                 ServerConnector connector = new ServerConnector(server, factory);
295                 connector.setSoLingerTime(-1);
296                 connector.setIdleTimeout(30000);
297                 connector.setPort(params.securePort);
41124c 298                 String bindInterface = settings.getString(Keys.server.httpsBindInterface, null);
EM 299                 if (!StringUtils.isEmpty(bindInterface)) {
300                     logger.warn(MessageFormat.format(
9ef027 301                             "Binding HTTPS transport on port {0,number,0} to {1}", params.securePort,
41124c 302                             bindInterface));
9ef027 303                     connector.setHost(bindInterface);
41124c 304                 }
EM 305                 if (params.securePort < 1024 && !isWindows()) {
306                     logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
307                 }
9ef027 308
JM 309                 server.addConnector(connector);
41124c 310             } else {
EM 311                 logger.warn("Failed to find or load Keystore?");
9ef027 312                 logger.warn("HTTPS transport DISABLED.");
41124c 313             }
EM 314         }
315
9ef027 316         // conditionally configure the http transport
JM 317         if (params.port > 0) {
318             /*
319              * HTTP
320              */
321             logger.info("Setting up HTTP transport on port " + params.port);
322
323             HttpConfiguration httpConfig = new HttpConfiguration();
324             if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) {
325                 httpConfig.setSecureScheme("https");
326                 httpConfig.setSecurePort(params.securePort);
41124c 327             }
9ef027 328             httpConfig.setSendServerVersion(false);
JM 329             httpConfig.setSendDateHeader(false);
330
331             ServerConnector connector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
332             connector.setSoLingerTime(-1);
333             connector.setIdleTimeout(30000);
334             connector.setPort(params.port);
335             String bindInterface = settings.getString(Keys.server.httpBindInterface, null);
336             if (!StringUtils.isEmpty(bindInterface)) {
337                 logger.warn(MessageFormat.format("Binding HTTP transport on port {0,number,0} to {1}",
338                         params.port, bindInterface));
339                 connector.setHost(bindInterface);
340             }
341             if (params.port < 1024 && !isWindows()) {
41124c 342                 logger.warn("Gitblit needs to run with ROOT permissions for ports < 1024!");
EM 343             }
9ef027 344
JM 345             server.addConnector(connector);
41124c 346         }
EM 347
348         // tempDir is where the embedded Gitblit web application is expanded and
349         // where Jetty creates any necessary temporary files
350         File tempDir = com.gitblit.utils.FileUtils.resolveParameter(Constants.baseFolder$, baseFolder, params.temp);
351         if (tempDir.exists()) {
352             try {
353                 FileUtils.delete(tempDir, FileUtils.RECURSIVE | FileUtils.RETRY);
354             } catch (IOException x) {
355                 logger.warn("Failed to delete temp dir " + tempDir.getAbsolutePath(), x);
356             }
357         }
358         if (!tempDir.mkdirs()) {
359             logger.warn("Failed to create temp dir " + tempDir.getAbsolutePath());
360         }
361
362         // Get the execution path of this class
363         // We use this to set the WAR path.
364         ProtectionDomain protectionDomain = GitBlitServer.class.getProtectionDomain();
365         URL location = protectionDomain.getCodeSource().getLocation();
366
367         // Root WebApp Context
368         WebAppContext rootContext = new WebAppContext();
369         rootContext.setContextPath(settings.getString(Keys.server.contextPath, "/"));
370         rootContext.setServer(server);
371         rootContext.setWar(location.toExternalForm());
372         rootContext.setTempDirectory(tempDir);
373
374         // Set cookies HttpOnly so they are not accessible to JavaScript engines
375         HashSessionManager sessionManager = new HashSessionManager();
376         sessionManager.setHttpOnly(true);
377         // Use secure cookies if only serving https
378         sessionManager.setSecureRequestOnly(params.port <= 0 && params.securePort > 0);
379         rootContext.getSessionHandler().setSessionManager(sessionManager);
380
381         // Ensure there is a defined User Service
382         String realmUsers = params.userService;
383         if (StringUtils.isEmpty(realmUsers)) {
384             logger.error(MessageFormat.format("PLEASE SPECIFY {0}!!", Keys.realm.userService));
385             return;
386         }
387
388         // Override settings from the command-line
389         settings.overrideSetting(Keys.realm.userService, params.userService);
390         settings.overrideSetting(Keys.git.repositoriesFolder, params.repositoriesFolder);
391         settings.overrideSetting(Keys.git.daemonPort, params.gitPort);
392         settings.overrideSetting(Keys.git.sshPort, params.sshPort);
393
394         // Start up an in-memory LDAP server, if configured
395         try {
396             if (!StringUtils.isEmpty(params.ldapLdifFile)) {
397                 File ldifFile = new File(params.ldapLdifFile);
398                 if (ldifFile != null && ldifFile.exists()) {
399                     URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));
400                     String firstLine = new Scanner(ldifFile).nextLine();
401                     String rootDN = firstLine.substring(4);
402                     String bindUserName = settings.getString(Keys.realm.ldap.username, "");
403                     String bindPassword = settings.getString(Keys.realm.ldap.password, "");
404
405                     // Get the port
406                     int port = ldapUrl.getPort();
407                     if (port == -1)
408                         port = 389;
409
410                     InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(rootDN);
411                     config.addAdditionalBindCredentials(bindUserName, bindPassword);
412                     config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("default", port));
413                     config.setSchema(null);
414
415                     InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
416                     ds.importFromLDIF(true, new LDIFReader(ldifFile));
417                     ds.startListening();
418
419                     logger.info("LDAP Server started at ldap://localhost:" + port);
420                 }
421             }
422         } catch (Exception e) {
423             // Completely optional, just show a warning
424             logger.warn("Unable to start LDAP server", e);
425         }
426
427         // Set the server's contexts
428         server.setHandler(rootContext);
429
430         // redirect HTTP requests to HTTPS
431         if (params.port > 0 && params.securePort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true)) {
432             logger.info(String.format("Configuring automatic http(%1$s) -> https(%2$s) redirects", params.port, params.securePort));
433             // Create the internal mechanisms to handle secure connections and redirects
434             Constraint constraint = new Constraint();
435             constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);
436
437             ConstraintMapping cm = new ConstraintMapping();
438             cm.setConstraint(constraint);
439             cm.setPathSpec("/*");
440
441             ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
442             sh.setConstraintMappings(new ConstraintMapping[] { cm });
443
444             // Configure this context to use the Security Handler defined before
445             rootContext.setHandler(sh);
446         }
447
448         // Setup the Gitblit context
449         GitblitContext gitblit = newGitblit(settings, baseFolder);
450         rootContext.addEventListener(gitblit);
451
452         try {
453             // start the shutdown monitor
454             if (params.shutdownPort > 0) {
455                 Thread shutdownMonitor = new ShutdownMonitorThread(server, params);
456                 shutdownMonitor.start();
457             }
458
459             // start Jetty
460             server.start();
461             server.join();
462         } catch (Exception e) {
463             e.printStackTrace();
464             System.exit(100);
465         }
466     }
467
468     protected GitblitContext newGitblit(IStoredSettings settings, File baseFolder) {
469         return new GitblitContext(settings, baseFolder);
470     }
471
472     /**
473      * Tests to see if the operating system is Windows.
474      *
475      * @return true if this is a windows machine
476      */
477     private boolean isWindows() {
478         return System.getProperty("os.name").toLowerCase().indexOf("windows") > -1;
479     }
480
481     /**
482      * The ShutdownMonitorThread opens a socket on a specified port and waits
483      * for an incoming connection. When that connection is accepted a shutdown
484      * message is issued to the running Jetty server.
485      *
486      * @author James Moger
487      *
488      */
489     private static class ShutdownMonitorThread extends Thread {
490
491         private final ServerSocket socket;
492
493         private final Server server;
494
495         private final Logger logger = LoggerFactory.getLogger(ShutdownMonitorThread.class);
496
497         public ShutdownMonitorThread(Server server, Params params) {
498             this.server = server;
499             setDaemon(true);
500             setName(Constants.NAME + " Shutdown Monitor");
501             ServerSocket skt = null;
502             try {
503                 skt = new ServerSocket(params.shutdownPort, 1, InetAddress.getByName("127.0.0.1"));
504             } catch (Exception e) {
505                 logger.warn("Could not open shutdown monitor on port " + params.shutdownPort, e);
506             }
507             socket = skt;
508         }
509
510         @Override
511         public void run() {
2f3618 512             // Only run if the socket was able to be created (not already in use, failed to bind, etc.)
JJ 513             if (null != socket) {
514                 logger.info("Shutdown Monitor listening on port " + socket.getLocalPort());
515                 Socket accept;
516                 try {
517                     accept = socket.accept();
518                     BufferedReader reader = new BufferedReader(new InputStreamReader(
519                             accept.getInputStream()));
520                     reader.readLine();
521                     logger.info(Constants.BORDER);
522                     logger.info("Stopping " + Constants.NAME);
523                     logger.info(Constants.BORDER);
524                     server.stop();
525                     server.setStopAtShutdown(false);
526                     accept.close();
527                     socket.close();
528                 } catch (Exception e) {
529                     logger.warn("Failed to shutdown Jetty", e);
530                 }
41124c 531             }
EM 532         }
533     }
534
535     /**
536      * Parameters class for GitBlitServer.
537      */
538     public static class Params {
539
540         public static String baseFolder;
541
542         private final FileSettings FILESETTINGS = new FileSettings(new File(baseFolder, Constants.PROPERTIES_FILE).getAbsolutePath());
543
544         /*
545          * Server parameters
546          */
547         @Option(name = "--help", aliases = { "-h"}, usage = "Show this help")
548         public Boolean help = false;
549
550         @Option(name = "--stop", usage = "Stop Server")
551         public Boolean stop = false;
552
553         @Option(name = "--tempFolder", usage = "Folder for server to extract built-in webapp", metaVar="PATH")
554         public String temp = FILESETTINGS.getString(Keys.server.tempFolder, "temp");
555
556         @Option(name = "--dailyLogFile", usage = "Log to a rolling daily log file INSTEAD of stdout.")
557         public Boolean dailyLogFile = false;
558
559         /*
560          * GIT Servlet Parameters
561          */
562         @Option(name = "--repositoriesFolder", usage = "Git Repositories Folder", metaVar="PATH")
563         public String repositoriesFolder = FILESETTINGS.getString(Keys.git.repositoriesFolder,
564                 "git");
565
566         /*
567          * Authentication Parameters
568          */
569         @Option(name = "--userService", usage = "Authentication and Authorization Service (filename or fully qualified classname)")
570         public String userService = FILESETTINGS.getString(Keys.realm.userService,
571                 "users.conf");
572
573         /*
574          * JETTY Parameters
575          */
576         @Option(name = "--httpPort", usage = "HTTP port for to serve. (port <= 0 will disable this connector)", metaVar="PORT")
577         public Integer port = FILESETTINGS.getInteger(Keys.server.httpPort, 0);
578
579         @Option(name = "--httpsPort", usage = "HTTPS port to serve.  (port <= 0 will disable this connector)", metaVar="PORT")
580         public Integer securePort = FILESETTINGS.getInteger(Keys.server.httpsPort, 8443);
581
582         @Option(name = "--gitPort", usage = "Git Daemon port to serve.  (port <= 0 will disable this connector)", metaVar="PORT")
583         public Integer gitPort = FILESETTINGS.getInteger(Keys.git.daemonPort, 9418);
584
585         @Option(name = "--sshPort", usage = "Git SSH port to serve.  (port <= 0 will disable this connector)", metaVar = "PORT")
586         public Integer sshPort = FILESETTINGS.getInteger(Keys.git.sshPort, 29418);
587
588         @Option(name = "--alias", usage = "Alias of SSL certificate in keystore for serving https.", metaVar="ALIAS")
589         public String alias = FILESETTINGS.getString(Keys.server.certificateAlias, "");
590
591         @Option(name = "--storePassword", usage = "Password for SSL (https) keystore.", metaVar="PASSWORD")
592         public String storePassword = FILESETTINGS.getString(Keys.server.storePassword, "");
593
594         @Option(name = "--shutdownPort", usage = "Port for Shutdown Monitor to listen on. (port <= 0 will disable this monitor)", metaVar="PORT")
595         public Integer shutdownPort = FILESETTINGS.getInteger(Keys.server.shutdownPort, 8081);
596
597         @Option(name = "--requireClientCertificates", usage = "Require client X509 certificates for https connections.")
598         public Boolean requireClientCertificates = FILESETTINGS.getBoolean(Keys.server.requireClientCertificates, false);
599
600         /*
601          * Setting overrides
602          */
603         @Option(name = "--settings", usage = "Path to alternative settings", metaVar="FILE")
604         public String settingsfile;
605
606         @Option(name = "--ldapLdifFile", usage = "Path to LDIF file.  This will cause an in-memory LDAP server to be started according to gitblit settings", metaVar="FILE")
607         public String ldapLdifFile;
608
609     }
5fe7df 610 }