DownloadService.java 13.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/*
 * The MIT License
 *
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
K
kohsuke 已提交
24 25 26 27 28
package hudson.model;

import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
29
import hudson.ProxyConfiguration;
30 31
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
K
kohsuke 已提交
32 33 34 35
import hudson.util.QuotedStringTokenizer;
import hudson.util.TextFile;
import java.io.File;
import java.io.IOException;
36 37 38
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
K
kohsuke 已提交
39
import java.util.logging.Logger;
40 41 42 43
import jenkins.security.DownloadSettings;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
K
kohsuke 已提交
44
import net.sf.json.JSONObject;
45 46 47
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
48
import org.kohsuke.stapler.StaplerRequest;
49
import org.kohsuke.stapler.StaplerResponse;
K
kohsuke 已提交
50

K
Kohsuke Kawaguchi 已提交
51
import static hudson.util.TimeUnit2.DAYS;
52
import org.apache.commons.io.IOUtils;
K
Kohsuke Kawaguchi 已提交
53

K
kohsuke 已提交
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
/**
 * Service for plugins to periodically retrieve update data files
 * (like the one in the update center) through browsers.
 *
 * <p>
 * Because the retrieval of the file goes through XmlHttpRequest,
 * we cannot reliably pass around binary.
 *
 * @author Kohsuke Kawaguchi
 */
@Extension
public class DownloadService extends PageDecorator {
    /**
     * Builds up an HTML fragment that starts all the download jobs.
     */
    public String generateFragment() {
70 71 72
        if (!DownloadSettings.get().isUseBrowser()) {
            return "";
        }
73
    	if (neverUpdate) return "";
K
Kohsuke Kawaguchi 已提交
74 75
        if (doesNotSupportPostMessage())  return "";

K
kohsuke 已提交
76
        StringBuilder buf = new StringBuilder();
77
        if(Jenkins.getInstance().hasPermission(Jenkins.READ)) {
78 79
            long now = System.currentTimeMillis();
            for (Downloadable d : Downloadable.all()) {
80
                if(d.getDue()<now && d.lastAttempt+10*1000<now) {
81 82 83
                    buf.append("<script>")
                       .append("Behaviour.addLoadEvent(function() {")
                       .append("  downloadService.download(")
84 85
                       .append(QuotedStringTokenizer.quote(d.getId()))
                       .append(',')
86
                       .append(QuotedStringTokenizer.quote(mapHttps(d.getUrl())))
87
                       .append(',')
88
                       .append("{version:"+QuotedStringTokenizer.quote(Jenkins.VERSION)+'}')
89 90 91
                       .append(',')
                       .append(QuotedStringTokenizer.quote(Stapler.getCurrentRequest().getContextPath()+'/'+getUrl()+"/byId/"+d.getId()+"/postBack"))
                       .append(',')
92 93 94
                       .append("null);")
                       .append("});")
                       .append("</script>");
95
                    d.lastAttempt = now;
96
                }
K
kohsuke 已提交
97 98 99 100 101
            }
        }
        return buf.toString();
    }

K
Kohsuke Kawaguchi 已提交
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
    private boolean doesNotSupportPostMessage() {
        StaplerRequest req = Stapler.getCurrentRequest();
        if (req==null)      return false;

        String ua = req.getHeader("User-Agent");
        if (ua==null)       return false;

        // according to http://caniuse.com/#feat=x-doc-messaging, IE <=7 doesn't support pstMessage
        // see http://www.useragentstring.com/pages/Internet%20Explorer/ for user agents

        // we want to err on the cautious side here.
        // Because of JENKINS-15105, we can't serve signed metadata from JSON, which means we need to be
        // using a modern browser as a vehicle to request these data. This check is here to prevent Jenkins
        // from using older browsers that are known not to support postMessage as the vehicle.
        return ua.contains("Windows") && (ua.contains(" MSIE 5.") || ua.contains(" MSIE 6.") || ua.contains(" MSIE 7."));
    }

119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    private String mapHttps(String url) {
        /*
            HACKISH:

            Loading scripts in HTTP from HTTPS pages cause browsers to issue a warning dialog.
            The elegant way to solve the problem is to always load update center from HTTPS,
            but our backend mirroring scheme isn't ready for that. So this hack serves regular
            traffic in HTTP server, and only use HTTPS update center for Jenkins in HTTPS.

            We'll monitor the traffic to see if we can sustain this added traffic.
         */
        if (url.startsWith("http://updates.jenkins-ci.org/") && Jenkins.getInstance().isRootUrlSecure())
            return "https"+url.substring(4);
        return url;
    }

K
kohsuke 已提交
135 136 137 138 139 140 141 142 143 144 145
    /**
     * Gets {@link Downloadable} by its ID.
     * Used to bind them to URL.
     */
    public Downloadable getById(String id) {
        for (Downloadable d : Downloadable.all())
            if(d.getId().equals(id))
                return d;
        return null;
    }

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
    /**
     * 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();
        }
    }

K
kohsuke 已提交
174 175 176 177
    /**
     * Represents a periodically updated JSON data file obtained from a remote URL.
     *
     * <p>
178
     * This mechanism is one of the basis of the update center, which involves fetching
K
kohsuke 已提交
179 180 181 182
     * up-to-date data file.
     *
     * @since 1.305
     */
183
    public static class Downloadable implements ExtensionPoint {
K
kohsuke 已提交
184 185 186 187
        private final String id;
        private final String url;
        private final long interval;
        private volatile long due=0;
188
        private volatile long lastAttempt=Long.MIN_VALUE;
K
kohsuke 已提交
189 190 191 192

        /**
         *
         * @param url
193
         *      URL relative to {@link UpdateCenter#getDefaultBaseUrl()}.
K
kohsuke 已提交
194
         *      So if this string is "foo.json", the ultimate URL will be
195
         *      something like "http://updates.jenkins-ci.org/updates/foo.json"
K
kohsuke 已提交
196 197 198 199
         *
         *      For security and privacy reasons, we don't allow the retrieval
         *      from random locations.
         */
200
        public Downloadable(String id, String url, long interval) {
K
kohsuke 已提交
201 202 203 204 205
            this.id = id;
            this.url = url;
            this.interval = interval;
        }

K
Kohsuke Kawaguchi 已提交
206 207 208 209 210 211
        public Downloadable() {
            this.id = getClass().getName().replace('$','.');
            this.url = this.id+".json";
            this.interval = DEFAULT_INTERVAL;
        }

212 213 214 215 216 217 218 219 220 221 222 223
        /**
         * Uses the class name as an ID.
         */
        public Downloadable(Class id) {
            this(id.getName().replace('$','.'));
        }

        public Downloadable(String id) {
            this(id,id+".json");
        }

        public Downloadable(String id, String url) {
K
Kohsuke Kawaguchi 已提交
224
            this(id,url, DEFAULT_INTERVAL);
225 226
        }

K
kohsuke 已提交
227 228 229 230 231 232 233 234
        public String getId() {
            return id;
        }

        /**
         * URL to download.
         */
        public String getUrl() {
235
            return Jenkins.getInstance().getUpdateCenter().getDefaultBaseUrl()+"updates/"+url;
K
kohsuke 已提交
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
        }

        /**
         * How often do we retrieve the new image?
         *
         * @return
         *      number of milliseconds between retrieval.
         */
        public long getInterval() {
            return interval;
        }

        /**
         * This is where the retrieved file will be stored.
         */
        public TextFile getDataFile() {
252
            return new TextFile(new File(Jenkins.getInstance().getRootDir(),"updates/"+id));
K
kohsuke 已提交
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
        }

        /**
         * When shall we retrieve this file next time?
         */
        public long getDue() {
            if(due==0)
                // if the file doesn't exist, this code should result
                // in a very small (but >0) due value, which should trigger
                // the retrieval immediately.
                due = getDataFile().file.lastModified()+interval;
            return due;
        }

        /**
         * Loads the current file into JSON and returns it, or null
         * if no data exists.
         */
        public JSONObject getData() throws IOException {
            TextFile df = getDataFile();
            if(df.exists())
274 275 276 277
                try {
                    return JSONObject.fromObject(df.read());
                } catch (JSONException e) {
                    df.delete(); // if we keep this file, it will cause repeated failures
278
                    throw new IOException("Failed to parse "+df+" into JSON",e);
279
                }
K
kohsuke 已提交
280 281 282 283 284 285
            return null;
        }

        /**
         * This is where the browser sends us the data. 
         */
286
        public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException {
287 288 289
            if (!DownloadSettings.get().isUseBrowser()) {
                throw new IOException("not allowed");
            }
K
kohsuke 已提交
290
            long dataTimestamp = System.currentTimeMillis();
291 292 293
            due = dataTimestamp+getInterval();  // success or fail, don't try too often

            String json = IOUtils.toString(req.getInputStream(),"UTF-8");
294 295 296 297 298 299 300 301 302
            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 {
303 304
            JSONObject o = JSONObject.fromObject(json);

305
            if (DownloadSettings.get().isCheckSignature()) {
306 307
                FormValidation e = new JSONSignatureValidator("downloadable '"+id+"'").verifySignature(o);
                if (e.kind!= Kind.OK) {
308
                    return e;
309 310 311
                }
            }

K
kohsuke 已提交
312
            TextFile df = getDataFile();
313
            df.write(json);
K
kohsuke 已提交
314 315
            df.file.setLastModified(dataTimestamp);
            LOGGER.info("Obtained the updated data file for "+id);
316 317
            return FormValidation.ok();
        }
318

319 320 321
        @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());
K
kohsuke 已提交
322 323 324 325 326 327
        }

        /**
         * Returns all the registered {@link Downloadable}s.
         */
        public static ExtensionList<Downloadable> all() {
328
            return Jenkins.getInstance().getExtensionList(Downloadable.class);
K
kohsuke 已提交
329 330
        }

331 332 333 334 335 336 337 338 339 340 341
        /**
         * Returns the {@link Downloadable} that has the given ID.
         */
        public static Downloadable get(String id) {
            for (Downloadable d : all()) {
                if(d.id.equals(id))
                    return d;
            }
            return null;
        }

K
kohsuke 已提交
342
        private static final Logger LOGGER = Logger.getLogger(Downloadable.class.getName());
K
Kohsuke Kawaguchi 已提交
343 344
        private static final long DEFAULT_INTERVAL =
                Long.getLong(Downloadable.class.getName()+".defaultInterval", DAYS.toMillis(1));
K
kohsuke 已提交
345
    }
346 347

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

349
    /** Now used only to set default value of, and enable UI switching of, {@link DownloadSettings#setIgnoreSignature}. */
350
    public static boolean signatureCheck = !Boolean.getBoolean(DownloadService.class.getName()+".noSignatureCheck");
K
kohsuke 已提交
351
}
352