Skip to content

Commit

Permalink
[FIXED JENKINS-19081] Offer the option of downloading metadata direct…
Browse files Browse the repository at this point in the history
…ly from the server.
  • Loading branch information
jglick committed Feb 27, 2014
1 parent f9953ad commit 1ac7775
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 64 deletions.
23 changes: 23 additions & 0 deletions core/src/main/java/hudson/PluginManager.java
Expand Up @@ -108,7 +108,12 @@
import org.xml.sax.helpers.DefaultHandler;

import static hudson.init.InitMilestone.*;
import hudson.model.DownloadService;
import hudson.util.FormValidation;
import static java.util.logging.Level.WARNING;
import jenkins.security.DownloadSettings;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Manages {@link PluginWrapper}s.
Expand Down Expand Up @@ -775,6 +780,24 @@ public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, Servl
}
}

@Restricted(NoExternalUse.class)
@RequirePOST public HttpResponse doCheckUpdatesServer() throws IOException {
for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) {
FormValidation v = site.updateDirectlyNow(DownloadSettings.get().isCheckSignature());
if (v.kind != FormValidation.Kind.OK) {
// TODO crude but enough for now
return v;
}
}
for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
FormValidation v = d.updateNow();
if (v.kind != FormValidation.Kind.OK) {
return v;
}
}
return HttpResponses.forwardToPreviousPage();
}

protected String identifyPluginShortName(File t) {
try {
JarFile j = new JarFile(t);
Expand Down
78 changes: 63 additions & 15 deletions core/src/main/java/hudson/model/DownloadService.java
Expand Up @@ -26,26 +26,30 @@
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.ProxyConfiguration;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import hudson.util.IOUtils;
import hudson.util.QuotedStringTokenizer;
import hudson.util.TextFile;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import org.kohsuke.stapler.Stapler;

import static hudson.util.TimeUnit2.DAYS;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.logging.Logger;

import jenkins.security.DownloadSettings;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import static hudson.util.TimeUnit2.DAYS;

/**
* Service for plugins to periodically retrieve update data files
* (like the one in the update center) through browsers.
Expand All @@ -62,6 +66,9 @@ public class DownloadService extends PageDecorator {
* Builds up an HTML fragment that starts all the download jobs.
*/
public String generateFragment() {
if (!DownloadSettings.get().isUseBrowser()) {
return "";
}
if (neverUpdate) return "";
if (doesNotSupportPostMessage()) return "";

Expand Down Expand Up @@ -135,6 +142,34 @@ public Downloadable getById(String id) {
return null;
}

/**
* Loads JSON from a JSONP URL.
* Metadata for downloadables and update centers is offered in two formats, both designed for download from the browser (predating {@link DownloadSettings}):
* HTML using {@code postMessage} for newer browsers, and JSONP as a fallback.
* Confusingly, the JSONP files are given the {@code *.json} file extension, when they are really JavaScript and should be {@code *.js}.
* This method extracts the JSON from a JSONP URL, since that is what we actually want when we download from the server.
* (Currently the true JSON is not published separately, and extracting from the {@code *.json.html} is more work.)
* @param src a URL to a JSONP file (typically including {@code id} and {@code version} query parameters)
* @return the embedded JSON text
* @throws IOException if either downloading or processing failed
*/
@Restricted(NoExternalUse.class)
public static String loadJSON(URL src) throws IOException {
InputStream is = ProxyConfiguration.open(src).getInputStream();
try {
String jsonp = IOUtils.toString(is, "UTF-8");
int start = jsonp.indexOf('{');
int end = jsonp.lastIndexOf('}');
if (start >= 0 && end > start) {
return jsonp.substring(start, end + 1);
} else {
throw new IOException("Could not find JSON in " + src);
}
} finally {
is.close();
}
}

/**
* Represents a periodically updated JSON data file obtained from a remote URL.
*
Expand Down Expand Up @@ -248,26 +283,41 @@ public JSONObject getData() throws IOException {
* This is where the browser sends us the data.
*/
public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (!DownloadSettings.get().isUseBrowser()) {
throw new IOException("not allowed");
}
long dataTimestamp = System.currentTimeMillis();
due = dataTimestamp+getInterval(); // success or fail, don't try too often

String json = IOUtils.toString(req.getInputStream(),"UTF-8");
FormValidation e = load(json, dataTimestamp);
if (e.kind != Kind.OK) {
LOGGER.severe(e.renderHtml());
throw e;
}
rsp.setContentType("text/plain"); // So browser won't try to parse response
}

private FormValidation load(String json, long dataTimestamp) throws IOException {
JSONObject o = JSONObject.fromObject(json);

if (signatureCheck) {
if (DownloadSettings.get().isCheckSignature()) {
FormValidation e = new JSONSignatureValidator("downloadable '"+id+"'").verifySignature(o);
if (e.kind!= Kind.OK) {
LOGGER.severe(e.renderHtml());
throw e;
return e;
}
}

TextFile df = getDataFile();
df.write(json);
df.file.setLastModified(dataTimestamp);
LOGGER.info("Obtained the updated data file for "+id);
return FormValidation.ok();
}

rsp.setContentType("text/plain"); // So browser won't try to parse response
@Restricted(NoExternalUse.class)
public FormValidation updateNow() throws IOException {
return load(loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), System.currentTimeMillis());
}

/**
Expand Down Expand Up @@ -295,9 +345,7 @@ public static Downloadable get(String id) {

public static boolean neverUpdate = Boolean.getBoolean(DownloadService.class.getName()+".never");

/**
* Off by default until we know this is reasonably working.
*/
/** @deprecated Use {@link DownloadSettings#setIgnoreSignature} instead. */
public static boolean signatureCheck = !Boolean.getBoolean(DownloadService.class.getName()+".noSignatureCheck");
}

3 changes: 2 additions & 1 deletion core/src/main/java/hudson/model/UpdateCenter.java
Expand Up @@ -88,6 +88,7 @@
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import jenkins.security.DownloadSettings;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
Expand Down Expand Up @@ -630,7 +631,7 @@ public List<Plugin> getUpdates() {
public List<FormValidation> updateAllSites() throws InterruptedException, ExecutionException {
List <Future<FormValidation>> futures = new ArrayList<Future<FormValidation>>();
for (UpdateSite site : getSites()) {
Future<FormValidation> future = site.updateDirectly(true);
Future<FormValidation> future = site.updateDirectly(DownloadSettings.get().isCheckSignature());
if (future != null) {
futures.add(future);
}
Expand Down
73 changes: 30 additions & 43 deletions core/src/main/java/hudson/model/UpdateSite.java
Expand Up @@ -27,33 +27,19 @@

import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.ProxyConfiguration;
import hudson.lifecycle.Lifecycle;
import hudson.model.UpdateCenter.UpdateCenterJob;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import hudson.util.HttpResponses;
import hudson.util.IOUtils;
import hudson.util.TextFile;
import static hudson.util.TimeUnit2.*;
import hudson.util.VersionNumber;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
Expand All @@ -67,9 +53,22 @@
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;

import static hudson.util.TimeUnit2.*;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.security.DownloadSettings;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;

/**
* Source of the update center information, like "http://jenkins-ci.org/update-center.json"
Expand Down Expand Up @@ -154,42 +153,30 @@ public long getDataTimestamp() {
* @return null if no updates are necessary, or the future result
* @since 1.502
*/
public Future<FormValidation> updateDirectly(final boolean signatureCheck) {
public @CheckForNull Future<FormValidation> updateDirectly(final boolean signatureCheck) {
if (! getDataFile().exists() || isDue()) {
return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable<FormValidation>() {

public FormValidation call() throws Exception {
URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8")
+ "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8"));
URLConnection conn = ProxyConfiguration.open(src);
InputStream is = conn.getInputStream();
try {
String uncleanJson = IOUtils.toString(is,"UTF-8");
int jsonStart = uncleanJson.indexOf("{\"");
if (jsonStart >= 0) {
uncleanJson = uncleanJson.substring(jsonStart);
int end = uncleanJson.lastIndexOf('}');
if (end>0)
uncleanJson = uncleanJson.substring(0,end+1);
return updateData(uncleanJson, signatureCheck);
} else {
throw new IOException("Could not find json in content of " +
"update center from url: "+src.toExternalForm());
}
} finally {
if (is != null)
is.close();
}
@Override public FormValidation call() throws Exception {
return updateDirectlyNow(signatureCheck);
}
});
}
} else {
return null;
}
}

@Restricted(NoExternalUse.class)
public @Nonnull FormValidation updateDirectlyNow(boolean signatureCheck) throws IOException {
return updateData(DownloadService.loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), signatureCheck);
}

/**
* This is the endpoint that receives the update center data file from the browser.
*/
public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
if (!DownloadSettings.get().isUseBrowser()) {
throw new IOException("not allowed");
}
return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true);
}

Expand Down
6 changes: 5 additions & 1 deletion core/src/main/java/hudson/util/FormValidation.java
Expand Up @@ -239,9 +239,13 @@ private static FormValidation _errorWithMarkup(final String message, final Kind
return ok();
return new FormValidation(kind, message) {
public String renderHtml() {
StaplerRequest req = Stapler.getCurrentRequest();
if (req == null) { // being called from some other context
return message;
}
// 1x16 spacer needed for IE since it doesn't support min-height
return "<div class="+ kind.name().toLowerCase(Locale.ENGLISH) +"><img src='"+
Stapler.getCurrentRequest().getContextPath()+ Jenkins.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
req.getContextPath()+ Jenkins.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
message+"</div>";
}
@Override public String toString() {
Expand Down

2 comments on commit 1ac7775

@daniel-beck
Copy link
Member

Choose a reason for hiding this comment

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

Report of brokenness related to this on the users list:

https://groups.google.com/forum/#!topic/jenkinsci-users/f_m6OVEy3is

@jglick
Copy link
Member Author

@jglick jglick commented on 1ac7775 Apr 30, 2014

Choose a reason for hiding this comment

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

That is just JENKINS-21984.

Please sign in to comment.