Code Monkey home page Code Monkey logo

Comments (22)

butchyyyy avatar butchyyyy commented on July 19, 2024 4

This is now released in version 2.1.1. Please note that it can take up to 24 hours for the new version to show in Jenkins update centre.

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024 1

Just tried it. It works. All good now.
Thanks a lot !

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

@eXtrem0us Version 2 sends a basic authorization header on every request to authenticate with the Zulip APIs same as previous versions.

Any chance you have a proxy requiring authentication configured in Jenkins settings, but did not provide the credentials?

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Hi
I just upgraded to zulip plugin version 2 and I have experienced exactly the same issue and stack trace.
My proxy settings are ok as I have been able to update properly plugins using that proxy.

I think my zulip credentials are ok.

context :

  • jenkins 2.397
  • zulip plugin 2.0.0

-update
I updated to jenkins 2.417. Same issue.
Unfortunately, I can't recreate a jenkins instance from scratch as there are other people using/working on it.

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

The only way I was able to reproduce the error was when I had a proxy requiring authentication configured in the Jenkins settings, but I did not provide the proxy user:

Proxy config Proxy error
image image

I have released a new version 2.1.0 with more verbose logging around authentication. If you could please update to it once it hits the update centre and then:

  1. Configure verbose logging for Zulip in your Jenkins instance

    image
  2. Try to run a Jenkins job that sends or notifies to Zulip chat.
  3. In an ideal scenario, you should see something like this:

    image

    Since this is not working for you you'll see something else, please post this something else here. Thanks!

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Hi @butchyyyy
I set up the logs and I had the following messages :
image

Our zulip is on the intranet and is an exception in the jenkins proxy host list.

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

Thanks for providing the logs @ebonnet31! In your case, the proxy is not involved due to no proxy host exclusion. Instead, it's the Zulip server returning 401/requesting authentication. The plugin does include credentials (basic authorization) on each Zulip API request so it means the credentials were not valid/rejected by the server.

I can reproduce this locally by entering the wrong credentials into my Jenkins instance:

image

Can you please double-check your global Jenkins configuration and verify that the Zulip User Email and Zulip API Key are correct / for the enabled Zulip bot?

I'll look into adjusting the error handling so it's more obvious what the problem is.

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Hi @butchyyyy
I have tried to reset the mail + api key.

To be sure of the parameters, I tried the parameters using a curl from the zulip example

# For stream messages
curl -X POST https://yourZulipDomain.zulipchat.com/api/v1/messages \
    -u BOT_EMAIL_ADDRESS:BOT_API_KEY \
    --data-urlencode type=stream \
    --data-urlencode 'to="Denmark"' \
    --data-urlencode topic=Castle \
    --data-urlencode 'content=I come not, friends, to steal away your hearts.'

The curl works with the same apiKey/mail set in jenkins zulip system properties.
But I still have the same jenkins/zulip error.

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

maybe an additional suggestion : provide a "test" button in the jenkins system configuration to check that the parameters are ok.

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Additional info : at one point, I entered a wrong url (http instead of https) to see the changes. I had a 301. However, I could see in the log the message :

We sent:api-key=XXXX&subject=AAAA&to=TTTT&type=stream&email=toto%40toto.com&content=Started+build+%2318+of+project+test-zulip

XXX/AAA/TTT/email being correct.

-> seems to show parameters are ok.

I entered back the right url => same exception message.

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

@ebonnet31 Thanks for verifying the credentials are correct and for providing all of the additional info. It looks like encoding the credentials into the basic authorization header does not work as expected in your case.

https://github.com/jenkinsci/zulip-plugin/blob/master/src/main/java/jenkins/plugins/zulip/Zulip.java#L146-L147

I have tried a bunch of things but had no luck reproducing 😞

Any idea on what could be different in your environment? e.g.:

  • Special characters in the bot name / access key
  • Charset the Jenkins JVM runs with
  • ...

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Hi @butchyyyy
jenkins run on windows with a default charset CP-1252. I have no idea for the the zulip server.
I noticed the line :

private static final Charset encodingCharset = StandardCharsets.UTF_8;

As a trial what do you suggest ? I can try to change my jenkins options to have a UTF8 charset for the VM. I'll be able to try that tomorrow.

from zulip-plugin.

JeffLi01 avatar JeffLi01 commented on July 19, 2024

I run into the exact problem.
I tried to reform the Zulip class to a standlone one (by removing dependencies for jenkins) to run with Java on local command line, and got the same error. But with a different approach I succeeded to send message to zulip server.

  1. First I tried this:

java.exe .\Zulip.java

// Zulip.java
import java.io.IOException;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Sends message to Zulip stream
 */
public class Zulip {

    private static final Charset encodingCharset = StandardCharsets.UTF_8;

    private String url;
    private String email;
    private String apiKey;
    private static Logger LOGGER = Logger.getLogger("");

    public Zulip(String url, String email, String apiKey) {
        super();
        if (url != null && url.length() > 0 && !url.endsWith("/")) {
            url = url + "/";
        }
        this.url = url;
        this.email = email;
        this.apiKey = apiKey;
    }

    protected void configureAuthenticator(HttpClient.Builder httpClientBuilder) {
        httpClientBuilder.authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                LOGGER.log(Level.FINE, "{0} authentication requested from {1}:{2}", new Object[] { getRequestorType(),
                        getRequestingHost(), getRequestingPort() });

                switch (getRequestorType()) {
                    default:
                        LOGGER.log(Level.FINE, "Unsupported authentication request");

                        return null;
                }
            }
        });
    }

    protected HttpClient getClient() throws MalformedURLException {
        HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
        configureAuthenticator(httpClientBuilder);
        return httpClientBuilder.build();
    }

    protected URI getApiEndpoint(String method) {
        StringBuilder uri = new StringBuilder();

        if (this.url.length() > 0) {
            uri.append(this.url);
            uri.append("api/v1/");
        } else {
            uri.append("https://api.zulip.com/v1/");
        }

        uri.append(method);

        return URI.create(uri.toString());
    }

    public String getApiKey() {
        return this.apiKey;
    }

    public String getEmail() {
        return this.email;
    }

    public HttpResponse<String> post(String method, Map<String, String> parameters) {
        try {
            String body = parameters.entrySet()
                    .stream()
                    .map(e -> encodeValue(e))
                    .collect(Collectors.joining("&"));
            System.out.println(body);

            String auth_info = this.getEmail() + ":" + this.getApiKey();
            String encoded_auth = Base64.getEncoder().encodeToString(auth_info.getBytes(encodingCharset));

            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(getApiEndpoint(method))
                    .header("User-Agent", "ZulipJenkins/0.1.2")
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Authorization", "Basic " + encoded_auth)
                    .POST(HttpRequest.BodyPublishers.ofString(body, encodingCharset))
                    .build();

            HttpClient client = getClient();

            HttpResponse<String> httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            if (httpResponse.statusCode() != 200) {
                LOGGER.log(Level.SEVERE,
                        "Error sending Zulip message:\nStatus:" + httpResponse.statusCode() + "\nBody:"
                                + httpResponse.body() + "\n\n" +
                                "We sent:" + body);
            }

            return httpResponse;
        } catch (IOException | InterruptedException e) {
            LOGGER.log(Level.SEVERE, "Error sending Zulip message: ", e);
        }

        return null;
    }

    public HttpResponse<String> sendStreamMessage(String stream, String subject, String message) {
        Map<String, String> parameters = new HashMap<String, String>();

        parameters.put("api-key", this.getApiKey());
        parameters.put("email", this.getEmail());
        parameters.put("type", "stream");
        parameters.put("to", stream);
        parameters.put("subject", subject);
        parameters.put("content", message);

        return post("messages", parameters);
    }

    private String encodeValue(Map.Entry<String, String> value) {
        String toEncode = value.getValue() != null ? value.getValue() : "";
        String encodedValue = URLEncoder.encode(toEncode, encodingCharset);

        return value.getKey() + "=" + encodedValue;
    }

    public static void main(String[] args) {
        LOGGER.setLevel(Level.SEVERE);

        String token = "EEfIvlMKhrWHLCK4UTRNM1U7lUlxnxw5";
        Zulip zulip = new Zulip("https://zulip.fw.trd", "[email protected]", token);
        zulip.sendStreamMessage("jenkins", "subject", "message");
    }
}
  1. Second, I tried this and succeeded. I can't tell the difference between the two method due to I am not familar with Java. Maybe you can find it out and solve the problem.

java .\ZulipBotMessageSender.java

// ZulipBotMessageSender.java
import java.io.IOException;
import java.net.Authenticator;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * Sends message to Zulip stream
 */
public class Zulip {

    private static final Charset encodingCharset = StandardCharsets.UTF_8;

    private String url;
    private String email;
    private String apiKey;
    private static Logger LOGGER = Logger.getLogger("");

    public Zulip(String url, String email, String apiKey) {
        super();
        if (url != null && url.length() > 0 && !url.endsWith("/")) {
            url = url + "/";
        }
        this.url = url;
        this.email = email;
        this.apiKey = apiKey;
    }

    protected void configureAuthenticator(HttpClient.Builder httpClientBuilder) {
        httpClientBuilder.authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                LOGGER.log(Level.FINE, "{0} authentication requested from {1}:{2}", new Object[] { getRequestorType(),
                        getRequestingHost(), getRequestingPort() });

                switch (getRequestorType()) {
                    default:
                        LOGGER.log(Level.FINE, "Unsupported authentication request");

                        return null;
                }
            }
        });
    }

    protected HttpClient getClient() throws MalformedURLException {
        HttpClient.Builder httpClientBuilder = HttpClient.newBuilder();
        configureAuthenticator(httpClientBuilder);
        return httpClientBuilder.build();
    }

    protected URI getApiEndpoint(String method) {
        StringBuilder uri = new StringBuilder();

        if (this.url.length() > 0) {
            uri.append(this.url);
            uri.append("api/v1/");
        } else {
            uri.append("https://api.zulip.com/v1/");
        }

        uri.append(method);

        return URI.create(uri.toString());
    }

    public String getApiKey() {
        return this.apiKey;
    }

    public String getEmail() {
        return this.email;
    }

    public HttpResponse<String> post(String method, Map<String, String> parameters) {
        try {
            String body = parameters.entrySet()
                    .stream()
                    .map(e -> encodeValue(e))
                    .collect(Collectors.joining("&"));
            System.out.println(body);

            String auth_info = this.getEmail() + ":" + this.getApiKey();
            String encoded_auth = Base64.getEncoder().encodeToString(auth_info.getBytes(encodingCharset));

            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(getApiEndpoint(method))
                    .header("User-Agent", "ZulipJenkins/0.1.2")
                    .header("Content-Type", "application/x-www-form-urlencoded")
                    .header("Authorization", "Basic " + encoded_auth)
                    .POST(HttpRequest.BodyPublishers.ofString(body, encodingCharset))
                    .build();

            HttpClient client = getClient();

            HttpResponse<String> httpResponse = client.send(httpRequest, HttpResponse.BodyHandlers.ofString());

            if (httpResponse.statusCode() != 200) {
                LOGGER.log(Level.SEVERE,
                        "Error sending Zulip message:\nStatus:" + httpResponse.statusCode() + "\nBody:"
                                + httpResponse.body() + "\n\n" +
                                "We sent:" + body);
            }

            return httpResponse;
        } catch (IOException | InterruptedException e) {
            LOGGER.log(Level.SEVERE, "Error sending Zulip message: ", e);
        }

        return null;
    }

    public HttpResponse<String> sendStreamMessage(String stream, String subject, String message) {
        Map<String, String> parameters = new HashMap<String, String>();

        parameters.put("api-key", this.getApiKey());
        parameters.put("email", this.getEmail());
        parameters.put("type", "stream");
        parameters.put("to", stream);
        parameters.put("subject", subject);
        parameters.put("content", message);

        return post("messages", parameters);
    }

    private String encodeValue(Map.Entry<String, String> value) {
        String toEncode = value.getValue() != null ? value.getValue() : "";
        String encodedValue = URLEncoder.encode(toEncode, encodingCharset);

        return value.getKey() + "=" + encodedValue;
    }

    public static void main(String[] args) {
        LOGGER.setLevel(Level.SEVERE);

        String token = "EEfIvlMKhrWHLCK4UTRNM1U7lUlxnxw5";
        Zulip zulip = new Zulip("https://zulip.fw.trd", "[email protected]", token);
        zulip.sendStreamMessage("jenkins", "subject", "message");
    }
}

from zulip-plugin.

JeffLi01 avatar JeffLi01 commented on July 19, 2024

I finally found out that the problem had something to do with below line:

configureAuthenticator(httpClientBuilder);

When I commented this line out, then the message got sent. Maybe current implemention is not suitable for deployment of Jenkins without proxy.

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

As a trial what do you suggest ? I can try to change my jenkins options to have a UTF8 charset for the VM. I'll be able to try that tomorrow.

That would be great. My test Jenkins instance runs with UTF-8 so I'll try to run it with CP-1252 and see if I can repro (won't manage before tomorrow as well).

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

I finally found out that the problem had something to do with below line:

configureAuthenticator(httpClientBuilder);

When I commented this line out, then the message got sent. Maybe current implemention is not suitable for deployment of Jenkins without proxy.

The authenticator should only be invoked when the server requests authentication (401 from Zulip server or 407 from proxy). My expectation would be that with authenticator removed, it will not fail with java.io.IOException: No credentials provided, but the message will still fail to send.

Removing the authenticator and sending the Proxy-Authorization header preemptively (if configured in Jenkins) is an option, but I'd like to get to the bottom of the problem before making changes like this.

from zulip-plugin.

JeffLi01 avatar JeffLi01 commented on July 19, 2024

I checked return status code from Zulip with curl but found none except 200.

curl -X POST https://zulip.fw.trd/api/v1/messages -u [email protected]:EEfIvlMKhrWHLCK4UTRNM1U7lUlxnxw5 --data-urlencode type=stream --data-urlencode 'to=jenkins' --data-urlencode topic=OpenBMC --data-urlencode 'content=OpenBMC Build #199 SUCCEED!' -H "User-Agent: ZulipJenkins/0.1.2" -iv

Then I add a
System.out.println("in getPasswordAuthentication");
in method getPasswordAuthentication() and the message got printed. So below assumption was not true.

The authenticator should only be invoked when the server requests authentication (401 from Zulip server or 407 from proxy).

from zulip-plugin.

JeffLi01 avatar JeffLi01 commented on July 19, 2024

I wonder if the root cause is mixing two authentication methods: header based and Authenticator based. The Authenticator is used but SERVER type is not handled. The header based authentication is ignored.

The code below works.

    protected void configureAuthenticator(HttpClient.Builder httpClientBuilder) {
        httpClientBuilder.authenticator(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                System.out.println("in getPasswordAuthentication");
                LOGGER.log(Level.FINE, "{0} authentication requested from {1}:{2}", new Object[] { getRequestorType(),
                        getRequestingHost(), getRequestingPort() });

                switch (getRequestorType()) {
                    // case PROXY:
                    //     ProxyConfiguration proxyConfiguration = Jenkins.get().proxy;

                    //     if (ZulipUtil.isValueSet(proxyConfiguration.getUserName())) {
                    //         LOGGER.log(Level.FINE, "Using proxy authentication username: {0}, password: ******",
                    //                 proxyConfiguration.getUserName());
                    //         return new PasswordAuthentication(proxyConfiguration.getUserName(),
                    //                 Secret.toString(proxyConfiguration.getSecretPassword()).toCharArray());
                    //     }

                    //     LOGGER.log(Level.FINE, "Proxy authentication not configured in Jenkins");

                    //     return null;
                    case SERVER:
                        LOGGER.log(Level.FINE, "SERVER authentication:");

                        return new PasswordAuthentication(email, apiKey.toCharArray());
                    default:
                        LOGGER.log(Level.FINE, "Unsupported authentication request");

                        return null;
                }
            }
        });
    }

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

Hi @butchyyyy
I have tried the UTF-8 encoding

set JENKINS_OPTS=-Dfile.encoding=UTF-8 -Djdk.http.auth.tunneling.disabledSchemes=""

Reset the parameters, so that file would be saved.
upgraded jenkins to the last version and updated plugins.
I still have the same error.

from zulip-plugin.

butchyyyy avatar butchyyyy commented on July 19, 2024

So... I'm mostly testing locally with Java 11 to make sure the plugin works with the minimal required Java version. I have now switched to Java 17 and I can reproduce the issue there...

I always considered Java to be good in backwards compatibility so I'm flabbergasted this is behaving differently in between versions 😞

The original idea of directly setting the header was to avoid an extra round-trip to the Zulip server, but since this is behaving inconsistently between Java versions, I'm gonna leave the auth headers in the sole discretion of the Authenticator.

I'll release 2.1.1 version today with a fix.

from zulip-plugin.

ebonnet31 avatar ebonnet31 commented on July 19, 2024

I didn't had suspicion on the JDK, but yes I am on the JDK 17 too.

from zulip-plugin.

eXtrem0us avatar eXtrem0us commented on July 19, 2024

Finally it works!
Thank you @butchyyyy !

from zulip-plugin.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.