James Moger
2012-11-06 798581cab5817310a1b9991dac3b10cd7813f86a
commit | author | age
892570 1 /*
JM 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  */
896c53 16 package com.gitblit;
JM 17
fa54be 18 import groovy.lang.Binding;
JM 19 import groovy.util.GroovyScriptEngine;
20
21 import java.io.BufferedReader;
22 import java.io.BufferedWriter;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStreamReader;
26 import java.io.OutputStreamWriter;
27 import java.text.MessageFormat;
28 import java.util.Collection;
367505 29 import java.util.Enumeration;
6cc1d4 30 import java.util.LinkedHashSet;
15640f 31 import java.util.List;
6cc1d4 32 import java.util.Set;
fa54be 33
JM 34 import javax.servlet.ServletConfig;
367505 35 import javax.servlet.ServletContext;
fa54be 36 import javax.servlet.ServletException;
JM 37 import javax.servlet.http.HttpServletRequest;
38
39 import org.eclipse.jgit.http.server.resolver.DefaultReceivePackFactory;
40 import org.eclipse.jgit.lib.PersonIdent;
41 import org.eclipse.jgit.lib.Repository;
15640f 42 import org.eclipse.jgit.revwalk.RevCommit;
fa54be 43 import org.eclipse.jgit.transport.PostReceiveHook;
JM 44 import org.eclipse.jgit.transport.PreReceiveHook;
45 import org.eclipse.jgit.transport.ReceiveCommand;
46 import org.eclipse.jgit.transport.ReceiveCommand.Result;
47 import org.eclipse.jgit.transport.ReceivePack;
48 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
49 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
15640f 53 import com.gitblit.Constants.AccessRestrictionType;
fa54be 54 import com.gitblit.models.RepositoryModel;
JM 55 import com.gitblit.models.UserModel;
a612e6 56 import com.gitblit.utils.ClientLogger;
fa54be 57 import com.gitblit.utils.HttpUtils;
15640f 58 import com.gitblit.utils.JGitUtils;
fa54be 59 import com.gitblit.utils.StringUtils;
JM 60
892570 61 /**
JM 62  * The GitServlet exists to force configuration of the JGit GitServlet based on
63  * the Gitblit settings from either gitblit.properties or from context
64  * parameters in the web.xml file.
fa54be 65  * 
JM 66  * It also implements and registers the Groovy hook mechanism.
892570 67  * 
JM 68  * Access to this servlet is protected by the GitFilter.
69  * 
70  * @author James Moger
71  * 
72  */
896c53 73 public class GitServlet extends org.eclipse.jgit.http.server.GitServlet {
JM 74
75     private static final long serialVersionUID = 1L;
fa54be 76
JM 77     private GroovyScriptEngine gse;
896c53 78
6cc1d4 79     private File groovyDir;
JM 80
fa54be 81     @Override
JM 82     public void init(ServletConfig config) throws ServletException {
367505 83         groovyDir = GitBlit.getGroovyScriptsFolder();
fa54be 84         try {
67d4f8 85             // set Grape root
JM 86             File grapeRoot = new File(GitBlit.getString(Keys.groovy.grapeFolder, "groovy/grape")).getAbsoluteFile();
87             grapeRoot.mkdirs();
88             System.setProperty("grape.root", grapeRoot.getAbsolutePath());
89             
90             gse = new GroovyScriptEngine(groovyDir.getAbsolutePath());            
fa54be 91         } catch (IOException e) {
JM 92             throw new ServletException("Failed to instantiate Groovy Script Engine!", e);
93         }
94
95         // set the Gitblit receive hook
96         setReceivePackFactory(new DefaultReceivePackFactory() {
97             @Override
98             public ReceivePack create(HttpServletRequest req, Repository db)
99                     throws ServiceNotEnabledException, ServiceNotAuthorizedException {
756117 100                 
JM 101                 // determine repository name from request
102                 String repositoryName = req.getPathInfo().substring(1);
103                 repositoryName = GitFilter.getRepositoryName(repositoryName);
104                 
fa54be 105                 GitblitReceiveHook hook = new GitblitReceiveHook();
756117 106                 hook.repositoryName = repositoryName;
fa54be 107                 hook.gitblitUrl = HttpUtils.getGitblitURL(req);
756117 108
JM 109                 ReceivePack rp = super.create(req, db);
fa54be 110                 rp.setPreReceiveHook(hook);
JM 111                 rp.setPostReceiveHook(hook);
20714a 112
JM 113                 // determine pushing user
114                 PersonIdent person = rp.getRefLogIdent();
115                 UserModel user = GitBlit.self().getUserModel(person.getName());
116                 if (user == null) {
117                     // anonymous push, create a temporary usermodel
118                     user = new UserModel(person.getName());
119                 }
120                 
121                 // enforce advanced ref permissions
122                 RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
123                 rp.setAllowCreates(user.canCreateRef(repository));
124                 rp.setAllowDeletes(user.canDeleteRef(repository));
125                 rp.setAllowNonFastForwards(user.canRewindRef(repository));
126                 
fa54be 127                 return rp;
JM 128             }
129         });
367505 130         super.init(new GitblitServletConfig(config));
JM 131     }
132
133     /**
d7905a 134      * Transitional wrapper class to configure the JGit 1.2 GitFilter. This
JM 135      * GitServlet will probably be replaced by a GitFilter so that Gitblit can
136      * serve Git repositories on the root URL and not a /git sub-url.
367505 137      * 
JM 138      * @author James Moger
139      * 
140      */
141     private class GitblitServletConfig implements ServletConfig {
142         final ServletConfig config;
143
144         GitblitServletConfig(ServletConfig config) {
145             this.config = config;
146         }
147
148         @Override
149         public String getServletName() {
150             return config.getServletName();
151         }
152
153         @Override
154         public ServletContext getServletContext() {
155             return config.getServletContext();
156         }
157
158         @Override
159         public String getInitParameter(String name) {
160             if (name.equals("base-path")) {
161                 return GitBlit.getRepositoriesFolder().getAbsolutePath();
162             } else if (name.equals("export-all")) {
163                 return "1";
164             }
165             return config.getInitParameter(name);
166         }
167
168         @Override
169         public Enumeration<String> getInitParameterNames() {
170             return config.getInitParameterNames();
171         }
fa54be 172     }
JM 173
174     /**
175      * The Gitblit receive hook allows for special processing on push events.
176      * That might include rejecting writes to specific branches or executing a
177      * script.
178      * 
179      * @author James Moger
180      * 
181      */
182     private class GitblitReceiveHook implements PreReceiveHook, PostReceiveHook {
183
184         protected final Logger logger = LoggerFactory.getLogger(GitblitReceiveHook.class);
185
756117 186         protected String repositoryName;
JM 187         
fa54be 188         protected String gitblitUrl;
JM 189
190         /**
191          * Instrumentation point where the incoming push event has been parsed,
192          * validated, objects created BUT refs have not been updated. You might
193          * use this to enforce a branch-write permissions model.
194          */
195         @Override
196         public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
756117 197             RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
15640f 198             UserModel user = getUserModel(rp);
JM 199             
200             if (repository.accessRestriction.atLeast(AccessRestrictionType.PUSH) && repository.verifyCommitter) {
201                 if (StringUtils.isEmpty(user.emailAddress)) {
202                     // emit warning if user does not have an email address 
203                     logger.warn(MessageFormat.format("Consider setting an email address for {0} ({1}) to improve committer verification.", user.getDisplayName(), user.username));
204                 }
205                 
206                 // Optionally enforce that the committer of the left parent chain
207                 // match the account being used to push the commits.
208                 // 
209                 // This requires all merge commits are executed with the "--no-ff"
210                 // option to force a merge commit even if fast-forward is possible.
211                 // This ensures that the chain of left parents has the commit
212                 // identity of the merging user.
213                 for (ReceiveCommand cmd : commands) {
214                     try {
215                         List<RevCommit> commits = JGitUtils.getRevLog(rp.getRepository(), cmd.getOldId().name(), cmd.getNewId().name());
216                         for (RevCommit commit : commits) {
217                             PersonIdent committer = commit.getCommitterIdent();
218                             if (!user.is(committer.getName(), committer.getEmailAddress())) {
219                                 String reason;
220                                 if (StringUtils.isEmpty(user.emailAddress)) {
221                                     // account does not have en email address
222                                     reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4})", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username);
223                                 } else {
224                                     // account has an email address
225                                     reason = MessageFormat.format("{0} by {1} <{2}> was not committed by {3} ({4}) <{5}>", commit.getId().name(), committer.getName(), StringUtils.isEmpty(committer.getEmailAddress()) ? "?":committer.getEmailAddress(), user.getDisplayName(), user.username, user.emailAddress);
226                                 }
227                                 cmd.setResult(Result.REJECTED_OTHER_REASON, reason);
228                                 break;
229                             }
230                         }
231                     } catch (Exception e) {
232                         logger.error("Failed to verify commits were made by pushing user", e);
233                     }
234                 }
235             }
236             
d7905a 237             Set<String> scripts = new LinkedHashSet<String>();
JM 238             scripts.addAll(GitBlit.self().getPreReceiveScriptsInherited(repository));
fa54be 239             scripts.addAll(repository.preReceiveScripts);
a612e6 240             runGroovy(repository, user, commands, rp, scripts);
fa54be 241             for (ReceiveCommand cmd : commands) {
JM 242                 if (!Result.NOT_ATTEMPTED.equals(cmd.getResult())) {
243                     logger.warn(MessageFormat.format("{0} {1} because \"{2}\"", cmd.getNewId()
244                             .getName(), cmd.getResult(), cmd.getMessage()));
245                 }
246             }
247
248             // Experimental
249             // runNativeScript(rp, "hooks/pre-receive", commands);
250         }
251
252         /**
253          * Instrumentation point where the incoming push has been applied to the
254          * repository. This is the point where we would trigger a Jenkins build
255          * or send an email.
256          */
257         @Override
258         public void onPostReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
259             if (commands.size() == 0) {
260                 logger.info("skipping post-receive hooks, no refs created, updated, or removed");
261                 return;
262             }
756117 263             RepositoryModel repository = GitBlit.self().getRepositoryModel(repositoryName);
d7905a 264             Set<String> scripts = new LinkedHashSet<String>();
JM 265             scripts.addAll(GitBlit.self().getPostReceiveScriptsInherited(repository));
fa54be 266             scripts.addAll(repository.postReceiveScripts);
JM 267             UserModel user = getUserModel(rp);
a612e6 268             runGroovy(repository, user, commands, rp, scripts);
20714a 269             for (ReceiveCommand cmd : commands) {
JM 270                 if (Result.OK.equals(cmd.getResult())) {
271                     // add some logging for important ref changes
272                     switch (cmd.getType()) {
273                     case DELETE:
274                         logger.info(MessageFormat.format("{0} DELETED {1} in {2} ({3})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name()));
275                         break;
276                     case CREATE:
277                         logger.info(MessageFormat.format("{0} CREATED {1} in {2}", user.username, cmd.getRefName(), repository.name));
278                         break;
279                     case UPDATE_NONFASTFORWARD:
280                         logger.info(MessageFormat.format("{0} UPDATED NON-FAST-FORWARD {1} in {2} (from {3} to {4})", user.username, cmd.getRefName(), repository.name, cmd.getOldId().name(), cmd.getNewId().name()));
281                         break;
282                     default:
283                         break;
284                     }
285                 }
286             }
287             
fa54be 288             // Experimental
JM 289             // runNativeScript(rp, "hooks/post-receive", commands);
290         }
291
292         /**
293          * Returns the UserModel for the user pushing the changes.
294          * 
295          * @param rp
296          * @return a UserModel
297          */
298         protected UserModel getUserModel(ReceivePack rp) {
299             PersonIdent person = rp.getRefLogIdent();
300             UserModel user = GitBlit.self().getUserModel(person.getName());
301             if (user == null) {
302                 // anonymous push, create a temporary usermodel
303                 user = new UserModel(person.getName());
6adf56 304                 user.isAuthenticated = false;
fa54be 305             }
JM 306             return user;
307         }
308
309         /**
310          * Runs the specified Groovy hook scripts.
311          * 
312          * @param repository
313          * @param user
314          * @param commands
315          * @param scripts
316          */
317         protected void runGroovy(RepositoryModel repository, UserModel user,
a612e6 318                 Collection<ReceiveCommand> commands, ReceivePack rp, Set<String> scripts) {
fa54be 319             if (scripts == null || scripts.size() == 0) {
JM 320                 // no Groovy scripts to execute
321                 return;
322             }
323
324             Binding binding = new Binding();
325             binding.setVariable("gitblit", GitBlit.self());
326             binding.setVariable("repository", repository);
6bb3b2 327             binding.setVariable("receivePack", rp);
fa54be 328             binding.setVariable("user", user);
JM 329             binding.setVariable("commands", commands);
330             binding.setVariable("url", gitblitUrl);
331             binding.setVariable("logger", logger);
a612e6 332             binding.setVariable("clientLogger", new ClientLogger(rp));
fa54be 333             for (String script : scripts) {
JM 334                 if (StringUtils.isEmpty(script)) {
335                     continue;
336                 }
6cc1d4 337                 // allow script to be specified without .groovy extension
JM 338                 // this is easier to read in the settings
339                 File file = new File(groovyDir, script);
340                 if (!file.exists() && !script.toLowerCase().endsWith(".groovy")) {
341                     file = new File(groovyDir, script + ".groovy");
342                     if (file.exists()) {
343                         script = file.getName();
344                     }
345                 }
fa54be 346                 try {
JM 347                     Object result = gse.run(script, binding);
348                     if (result instanceof Boolean) {
349                         if (!((Boolean) result)) {
350                             logger.error(MessageFormat.format(
351                                     "Groovy script {0} has failed!  Hook scripts aborted.", script));
352                             break;
353                         }
354                     }
355                 } catch (Exception e) {
356                     logger.error(
357                             MessageFormat.format("Failed to execute Groovy script {0}", script), e);
358                 }
359             }
360         }
361
362         /**
363          * Runs the native push hook script.
364          * 
365          * http://book.git-scm.com/5_git_hooks.html
366          * http://longair.net/blog/2011/04/09/missing-git-hooks-documentation/
367          * 
368          * @param rp
369          * @param script
370          * @param commands
371          */
372         @SuppressWarnings("unused")
373         protected void runNativeScript(ReceivePack rp, String script,
374                 Collection<ReceiveCommand> commands) {
375
376             Repository repository = rp.getRepository();
377             File scriptFile = new File(repository.getDirectory(), script);
378
379             int resultCode = 0;
380             if (scriptFile.exists()) {
381                 try {
382                     logger.debug("executing " + scriptFile);
383                     Process process = Runtime.getRuntime().exec(scriptFile.getAbsolutePath(), null,
384                             repository.getDirectory());
385                     BufferedReader reader = new BufferedReader(new InputStreamReader(
386                             process.getInputStream()));
387                     BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
388                             process.getOutputStream()));
389                     for (ReceiveCommand command : commands) {
390                         switch (command.getType()) {
391                         case UPDATE:
392                             // updating a ref
393                             writer.append(MessageFormat.format("{0} {1} {2}\n", command.getOldId()
394                                     .getName(), command.getNewId().getName(), command.getRefName()));
395                             break;
396                         case CREATE:
397                             // new ref
398                             // oldrev hard-coded to 40? weird.
399                             writer.append(MessageFormat.format("40 {0} {1}\n", command.getNewId()
400                                     .getName(), command.getRefName()));
401                             break;
402                         }
403                     }
404                     resultCode = process.waitFor();
405
406                     // read and buffer stdin
407                     // this is supposed to be piped back to the git client.
408                     // not sure how to do that right now.
409                     StringBuilder sb = new StringBuilder();
410                     String line = null;
411                     while ((line = reader.readLine()) != null) {
412                         sb.append(line).append('\n');
413                     }
414                     logger.debug(sb.toString());
415                 } catch (Throwable e) {
416                     resultCode = -1;
417                     logger.error(
418                             MessageFormat.format("Failed to execute {0}",
419                                     scriptFile.getAbsolutePath()), e);
420                 }
421             }
422
423             // reject push
424             if (resultCode != 0) {
425                 for (ReceiveCommand command : commands) {
426                     command.setResult(Result.REJECTED_OTHER_REASON, MessageFormat.format(
427                             "Native script {0} rejected push or failed",
428                             scriptFile.getAbsolutePath()));
429                 }
430             }
431         }
432     }
896c53 433 }