Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Confirm that characters which Windows requires be escaped in a batch
file can be used as characters in a git password (as used through https
with a username / password combination).
  • Loading branch information
MarkEWaite committed Jan 15, 2017
1 parent 0845f21 commit f7b7cb9
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 7 deletions.
17 changes: 10 additions & 7 deletions src/main/java/org/jenkinsci/plugins/gitclient/CliGitAPIImpl.java
Expand Up @@ -3,7 +3,6 @@

import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.UsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;

import edu.umd.cs.findbugs.annotations.CheckForNull;
Expand Down Expand Up @@ -52,8 +51,6 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

Expand Down Expand Up @@ -1536,7 +1533,8 @@ private File createSshKeyFile(File key, SSHUserPrivateKey sshUser) throws IOExce
return key;
}

private String quoteWindowsCredentials(String str) {
/* package protected for testability */
String quoteWindowsCredentials(String str) {
// Quote special characters for Windows Batch Files
// See: http://ss64.com/nt/syntax-esc.html
String quoted = str.replace("%", "%%")
Expand Down Expand Up @@ -1577,17 +1575,22 @@ private File createUnixSshAskpass(SSHUserPrivateKey sshUser) throws IOException
return ssh;
}

private File createWindowsStandardAskpass(StandardUsernamePasswordCredentials creds) throws IOException {
/* Package protected for testability */
File createWindowsBatFile(String userName, String password) throws IOException {
File askpass = File.createTempFile("pass", ".bat");
try (PrintWriter w = new PrintWriter(askpass, Charset.defaultCharset().toString())) {
w.println("@set arg=%~1");
w.println("@if (%arg:~0,8%)==(Username) echo " + quoteWindowsCredentials(creds.getUsername()));
w.println("@if (%arg:~0,8%)==(Password) echo " + quoteWindowsCredentials(Secret.toString(creds.getPassword())));
w.println("@if (%arg:~0,8%)==(Username) echo " + quoteWindowsCredentials(userName));
w.println("@if (%arg:~0,8%)==(Password) echo " + quoteWindowsCredentials(password));
}
askpass.setExecutable(true);
return askpass;
}

private File createWindowsStandardAskpass(StandardUsernamePasswordCredentials creds) throws IOException {
return createWindowsBatFile(creds.getUsername(), Secret.toString(creds.getPassword()));
}

private File createUnixStandardAskpass(StandardUsernamePasswordCredentials creds) throws IOException {
File askpass = File.createTempFile("pass", ".sh");
try (PrintWriter w = new PrintWriter(askpass, Charset.defaultCharset().toString())) {
Expand Down
@@ -0,0 +1,145 @@
package org.jenkinsci.plugins.gitclient;

import hudson.EnvVars;
import hudson.Launcher;
import hudson.model.TaskListener;
import hudson.util.ArgumentListBuilder;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.hasItems;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;

/**
* CliGitAPIImpl authorization specific tests.
*
* @author Mark Waite
*/
public class CliGitAPIImplAuthTest {

private final Launcher launcher;

public CliGitAPIImplAuthTest() {
launcher = new Launcher.LocalLauncher(TaskListener.NULL);
}

private CliGitAPIImpl git;

private final Random random = new Random();

private final String[] CARET_SPECIALS = {"&", "\\", "<", ">", "^", "|", " ", "\t"};
private final String[] PERCENT_SPECIALS = {"%"};

@Before
public void setUp() {
git = new CliGitAPIImpl("git", new File("."), TaskListener.NULL, new EnvVars());
}

@Test
public void testQuoteWindowsCredentials() throws Exception {
assertEquals("", git.quoteWindowsCredentials(""));
for (String special : CARET_SPECIALS) {
String expected = "^" + special;
assertEquals(expected, git.quoteWindowsCredentials(special));
assertEquals(expected + expected, git.quoteWindowsCredentials(special + special));
checkWindowsCommandOutput(special);
}
for (String special : PERCENT_SPECIALS) {
String expected = "%" + special;
assertEquals(expected, git.quoteWindowsCredentials(special));
assertEquals(expected + expected, git.quoteWindowsCredentials(special + special));
checkWindowsCommandOutput(special);
}
for (String startSpecial : CARET_SPECIALS) {
for (String endSpecial : PERCENT_SPECIALS) {
String middle = randomString();
String source = startSpecial + middle + endSpecial;
String expected = "^" + startSpecial + middle.replace(" ", "^ ") + "%" + endSpecial;
assertEquals(expected, git.quoteWindowsCredentials(source));
assertEquals(expected + expected, git.quoteWindowsCredentials(source + source));
checkWindowsCommandOutput(source);
}
}
for (String startSpecial : PERCENT_SPECIALS) {
for (String endSpecial : CARET_SPECIALS) {
String middle = randomString();
String source = startSpecial + middle + endSpecial;
String expected = "%" + startSpecial + middle.replace(" ", "^ ") + "^" + endSpecial;
assertEquals(expected, git.quoteWindowsCredentials(source));
assertEquals(expected + expected, git.quoteWindowsCredentials(source + source));
checkWindowsCommandOutput(source);
}
}
for (String startSpecial : PERCENT_SPECIALS) {
for (String endSpecial : PERCENT_SPECIALS) {
String middle = randomString();
String source = startSpecial + middle + endSpecial;
String expected = "%" + startSpecial + middle.replace(" ", "^ ") + "%" + endSpecial;
assertEquals(expected, git.quoteWindowsCredentials(source));
assertEquals(expected + expected, git.quoteWindowsCredentials(source + source));
checkWindowsCommandOutput(source);
}
}
}

private void checkWindowsCommandOutput(String password) throws Exception {
if (!isWindows() || password == null || password.isEmpty() || password.equals(" ") || password.equals("\t")) {
/* ArgumentListBuilder can't pass a single space or a single tab argument */
return;
}
String userName = "git";
File batFile = git.createWindowsBatFile(userName, password);
assertTrue(batFile.exists());
ArgumentListBuilder args = new ArgumentListBuilder(batFile.getAbsolutePath(), "Password");
String[] output = run(args);
assertThat(Arrays.asList(output), hasItems(password));
assertTrue("Failed to delete test batch file", batFile.delete());
assertFalse(batFile.exists());
}

private String[] run(ArgumentListBuilder args) throws IOException, InterruptedException {
String[] output;
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
ByteArrayOutputStream bytesErr = new ByteArrayOutputStream();
Launcher.ProcStarter p = launcher.launch().cmds(args).envs(new EnvVars()).stdout(bytesOut).stderr(bytesErr).pwd(new File("."));
int status = p.start().joinWithTimeout(1, TimeUnit.MINUTES, TaskListener.NULL);
String result = bytesOut.toString("UTF-8");
if (bytesErr.size() > 0) {
result = result + "\nstderr not empty:\n" + bytesErr.toString("UTF-8");
}
output = result.split("[\\n\\r]");
Assert.assertEquals(args.toString() + " command failed and reported '" + Arrays.toString(output) + "'", 0, status);
return output;
}

/* Strings may contain ' ' but should not contain other escaped chars */
private final String[] sourceData = {
"ЁЂЃЄЅ",
"Miloš Šafařík",
"ЌЍЎЏАБВГД",
"ЕЖЗИЙКЛМНОПРСТУФ",
"фхцчшщъыьэюя",
"الإطلاق",
"1;DROP TABLE users",
"C:",
"' OR '1'='1",
"He said, \"Hello!\", didn't he?",
"ZZ:",
"Roses are \u001b[0;31mred\u001b[0m"
};

private String randomString() {
int index = random.nextInt(sourceData.length);
return sourceData[index];
}

private boolean isWindows() {
return File.pathSeparatorChar == ';';
}
}

2 comments on commit f7b7cb9

@alexellis
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this in a build yet? @MarkEWaite .. I would like to test again if it fixes our issue.

@MarkEWaite
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until the build result expires, you can find a build on ci.jenkins.io

Please sign in to comment.