001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.server.config.repository; 018 019import static org.apache.juneau.commons.utils.FileUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.nio.file.*; 024import java.util.*; 025 026import org.apache.juneau.config.*; 027 028/** 029 * Command that fetches configuration from a Git repository for a given project and branch. 030 * 031 * <p> 032 * Clones or pulls the configured Git repo, then loads application and project-specific 033 * configuration files into a map of {@link ConfigItem} values. 034 */ 035public class GetConfiguration implements Command, GetValue<Map<String,ConfigItem>> { 036 037 private static final String APPLICATION = "APPLICATION"; 038 private static final String PROJECT = "PROJECT"; 039 private static final String EXT = ".cfg"; 040 041 // Serializes pull/checkout/pull on the single shared checkout so concurrent requests cannot interleave branches. 042 private static final Object GIT_LOCK = new Object(); 043 044 // Remote pulls are throttled: at most one fetch per interval (settable as [GitServer] pullInterval, in seconds). 045 private static final long DEFAULT_PULL_INTERVAL_SECONDS = 60; 046 private static long lastPullNanos; 047 private static boolean pulledOnce; 048 049 private final Map<String,ConfigItem> configs = new HashMap<>(); 050 051 @SuppressWarnings({ 052 "java:S1845" // Field name intentionally differs only by case from static field PROJECT 053 }) 054 private String project; 055 private final String branch; 056 057 /** 058 * Constructor. 059 * 060 * @param project Project name. 061 * @param branch Branch name. 062 */ 063 public GetConfiguration(String project, String branch) { 064 // Path variables are URL-decoded after the path is split, so an encoded '%2F' or '..' can survive (CWE-22). 065 validate("project", project, false); 066 validate("branch", branch, true); 067 this.branch = branch; 068 this.project = project; 069 } 070 071 /** {@inheritDoc} */ 072 @Override 073 public void execute() throws Exception { 074 075 String pathStr; 076 String git; 077 long pullInterval; 078 var config = Config.create().name("juneau-server-config.cfg").build(); 079 try { 080 pathStr = config.get("GitServer/pathLocal").orElse(null); 081 git = config.get("GitServer/gitRemote").orElse(null); 082 pullInterval = config.get("GitServer/pullInterval").asLong().orElse(DEFAULT_PULL_INTERVAL_SECONDS); 083 } finally { 084 config.close(); 085 } 086 087 // Fail fast with a clear message when the operator has not configured the local checkout directory, 088 // rather than surfacing an opaque NullPointerException from new File(null) below. 089 if (pathStr == null) 090 throw new IllegalStateException("Required configuration 'GitServer/pathLocal' is not set."); 091 092 // Trusted operator-configured local git checkout directory (GitServer/pathLocal). 093 // Serves as the confinement root for the project/application config file lookups below. 094 var root = new File(pathStr); 095 096 var fileDefaultStr = lcr(APPLICATION).concat(EXT); 097 var fileProjectStr = this.project.concat(EXT); 098 099 // The checkout is shared, so the git operations AND the file reads must happen under one lock 100 // (otherwise another request could switch branches between checkout and read). 101 synchronized (GIT_LOCK) { 102 try (var gitControl = new GitControl(pathStr, git)) { 103 104 var now = System.nanoTime(); 105 var due = ! pulledOnce || now - lastPullNanos >= pullInterval * 1_000_000_000L; 106 if (! root.isDirectory()) { 107 gitControl.cloneRepo(); 108 pulledOnce = true; 109 lastPullNanos = now; 110 } else if (due) { 111 gitControl.pullFromRepo(); 112 pulledOnce = true; 113 lastPullNanos = now; 114 } 115 116 gitControl.branch(branch); 117 if (due) 118 gitControl.pullFromRepo(); 119 } 120 121 readFiles(root, fileDefaultStr, fileProjectStr); 122 } 123 } 124 125 private void readFiles(File root, String fileDefaultStr, String fileProjectStr) throws IOException { 126 // Resolve the config file names under the checkout root through the shared boundary check 127 // so a crafted project name cannot escape the repo directory (empty = file absent). 128 var fileDefault = resolveSafely(root, fileDefaultStr).orElse(null); 129 if (fileDefault != null) { 130 var lines = new String(Files.readAllBytes(fileDefault.toPath())); 131 configs.put(APPLICATION, new ConfigItem(lines)); 132 } 133 134 var fileProject = resolveSafely(root, fileProjectStr).orElse(null); 135 if (fileProject != null) { 136 var linesProject = new String(Files.readAllBytes(fileProject.toPath())); 137 configs.put(PROJECT, new ConfigItem(linesProject)); 138 } 139 } 140 141 /** {@inheritDoc} */ 142 @Override 143 public Map<String,ConfigItem> get() { 144 return Map.copyOf(configs); 145 } 146 147 // Project becomes a file name, so no separators at all. Branch is only passed to git as a ref name, so 148 // legitimate names such as "feature/foo" are allowed, but '..', backslash and NUL are not. 149 private static void validate(String name, String value, boolean allowSlash) { 150 if (value == null || value.isEmpty() || value.contains("\\") || value.contains("..") || value.indexOf('\0') >= 0 || (! allowSlash && value.contains("/"))) 151 throw new IllegalArgumentException("Invalid " + name + " value."); 152 } 153}