diff --git a/build.gradle b/build.gradle index 3cc577d..9184488 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ dependencies { testImplementation 'io.mockk:mockk:1.9' implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.1' testCompile 'junit:junit:4.12' testCompile 'com.github.stefanbirkner:system-rules:1.19.0' diff --git a/src/main/java/com/github/sikandar/faktory/FaktoryClient.java b/src/main/java/com/github/sikandar/faktory/FaktoryClient.java index 770dc8f..897b3de 100644 --- a/src/main/java/com/github/sikandar/faktory/FaktoryClient.java +++ b/src/main/java/com/github/sikandar/faktory/FaktoryClient.java @@ -19,15 +19,18 @@ public class FaktoryClient { private final FaktoryConnection connection; public static String url = "tcp://localhost:7419"; - public FaktoryClient(final String urlParam) { + public FaktoryClient(final String urlParam, final String password) { url = urlParam == null ? url : urlParam; - connection = new FaktoryConnection(url); + connection = new FaktoryConnection(url, password); } - public FaktoryClient() { - this(System.getenv("FAKTORY_URL")); + public FaktoryClient(final String urlParam) { + this(urlParam, null); } + public FaktoryClient() { + this(System.getenv("FAKTORY_URL"), System.getenv("FAKTORY_PASSWORD")); + } public void push(final FaktoryJob job) throws IOException { final String payload = new Gson().toJson(job); @@ -35,6 +38,4 @@ public void push(final FaktoryJob job) throws IOException { connection.send("PUSH " + payload); connection.close(); } - - -} \ No newline at end of file +} diff --git a/src/main/java/com/github/sikandar/faktory/FaktoryConnection.java b/src/main/java/com/github/sikandar/faktory/FaktoryConnection.java index 0098ae2..fd7b657 100644 --- a/src/main/java/com/github/sikandar/faktory/FaktoryConnection.java +++ b/src/main/java/com/github/sikandar/faktory/FaktoryConnection.java @@ -1,11 +1,16 @@ package com.github.sikandar.faktory; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.URI; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; import java.util.regex.Pattern; /** @@ -20,29 +25,34 @@ public class FaktoryConnection { private static final Pattern HI_RECEIVED = Pattern.compile("\\+HI\\s\\{\"v\":\\d}"); private static final Pattern OK_RECEIVED = Pattern.compile("\\+OK"); + private static final String HELLO_WITH_NO_PASSWORD = "HELLO {\"v\":2}"; + private static final String HELLO_WITH_PASSWORD = "HELLO {\"pwdhash\":\"%s\",\"v\":2}"; private final URI url; + private final String password; private Socket socket; private BufferedReader fromServer; private DataOutputStream toServer; public FaktoryConnection(String url) { this.url = URI.create(url); + this.password = null; + } + + public FaktoryConnection(String url, String password) { + this.url = URI.create(url); + this.password = password; } /** * Method used to connect to Faktory using a Socket. */ - public void connect() throws IOException { socket = openSocket(); toServer = new DataOutputStream(socket.getOutputStream()); fromServer = new BufferedReader(new InputStreamReader(socket.getInputStream())); - if (!HI_RECEIVED.matcher(readFromSocket()).matches()) { - throw new FaktoryException("Invalid +HI, Expecting:" + HI_RECEIVED); - } - send("HELLO {\"v\":2}"); + send(getHelloMessage(readFromSocket())); } public void send(String message) throws IOException { @@ -71,4 +81,60 @@ private String readFromSocket() throws IOException { private void writeToSocket(String content) throws IOException { toServer.writeBytes(content + "\n"); } + + private String getHelloMessage(String hiMessage) throws IOException { + // If password not set in faktory you will receive "+HI {"v":2}" + if (password == null || password.length() == 0) { + if (!HI_RECEIVED.matcher(hiMessage).matches()) { + throw new FaktoryException("Invalid +HI, Expecting:" + HI_RECEIVED); + } + return HELLO_WITH_NO_PASSWORD; + } + + // If password set in faktory you will receive payload like + // "+HI {"v":2,"i":5171,"s":"5fb7c632793578c7"}" + // Parse the message to find version, salt and number of iteration + String jsonPayload = hiMessage.substring(hiMessage.indexOf('{')); + ObjectMapper objectMapper = new ObjectMapper(); + Map hiMap = objectMapper.readValue(jsonPayload, Map.class); + String salt = (String) hiMap.get("s"); + Integer iterations = (Integer) hiMap.get("i"); + + if (salt == null || iterations == null) { + throw new FaktoryException("Salt/Iterations cannot be null if password is set."); + } + + return String.format(HELLO_WITH_PASSWORD, hashPassword(password, salt, iterations)); + } + + private String hashPassword(String password, String salt, int iterations) { + try { + // Combine password and salt + String input = password + salt; + byte[] bytes = input.getBytes("UTF-8"); + + // Initialize SHA-256 MessageDigest + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(bytes); + + // Perform multiple iterations + for (int i = 1; i < iterations; i++) { + hash = digest.digest(hash); + } + + // Convert byte array to hexadecimal string + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + + return hexString.toString(); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new RuntimeException("Error while hashing password", e); + } + } } diff --git a/src/test/java/com/github/sikandar/faktory/FaktoryClientT.java b/src/test/java/com/github/sikandar/faktory/FaktoryClientT.java index 7378136..dc1b149 100644 --- a/src/test/java/com/github/sikandar/faktory/FaktoryClientT.java +++ b/src/test/java/com/github/sikandar/faktory/FaktoryClientT.java @@ -22,6 +22,7 @@ public void initializeClientWithoutUriAndWithoutEnvVars() { @Test public void initializeClientWithoutUriButWithEnvVars() { environmentVariables.set("FAKTORY_URL", "tcp://192.168.0.2:7419"); + environmentVariables.set("FAKTORY_PASSWORD", "password2Faktory"); FaktoryClient anotherClient = new FaktoryClient(); Assert.assertEquals("tcp://192.168.0.2:7419", anotherClient.url); @@ -33,6 +34,11 @@ public void initializeClientWithACustomUri() { Assert.assertEquals("tcp://192.168.0.1:7419", client.url); } + @Test + public void initializeClientWithACustomUriAndCustomPassword() { + FaktoryClient client = new FaktoryClient("tcp://192.168.0.1:7419", "password2Faktory"); + Assert.assertEquals("tcp://192.168.0.1:7419", client.url); + } @Test(expected = FaktoryException.class) public void pushFaktoryJobWithoutConnection() throws Exception {