PageRenderTime 194ms CodeModel.GetById 15ms app.highlight 170ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/Core/KSOmahaServer.m

http://macfuse.googlecode.com/
Objective C | 825 lines | 528 code | 120 blank | 177 comment | 102 complexity | 89dfafb91037c29eaaeb433db6bd0d18 MD5 | raw file
  1// Copyright 2009 Google Inc.
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     http://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#import "KSOmahaServer.h"
 16#include <sys/param.h>
 17#include <sys/mount.h>
 18#include <unistd.h>
 19#import "KSClientActives.h"
 20#import "KSFrameworkStats.h"
 21#import "KSStatsCollection.h"
 22#import "KSTicket.h"
 23#import "KSUpdateEngine.h"
 24#import "KSUpdateEngineParameters.h"
 25#import "KSUpdateInfo.h"
 26
 27// The brand code to report in the update request if there is no other
 28// brand code supplied via the ticket.
 29#define DEFAULT_BRAND_CODE @"GGLG"
 30
 31@interface KSOmahaServer (Private)
 32
 33// Walk the product actives dictionary provided in the UpdateEngine parameters
 34// and fill populate |actives_| for later use.
 35- (void)setupActives;
 36
 37// Return an NSDictionary with default settings for all params. This is a class
 38// method because it needs to be called before a class instance is initialized
 39// (i.e., before [super init...] is called).
 40+ (NSMutableDictionary *)defaultParams;
 41
 42// Builds the XML |document_| and |root_| for the Omaha request based on the
 43// stats contained in |stats|.
 44- (void)buildDocumentForStats:(KSStatsCollection *)stats;
 45
 46// begin construciton of the XML document used for a request
 47- (void)createRootAndDocument;
 48
 49// Returns an NSXMLElement for the specified application ID. If an element with
 50// appID is already attached to |root_|, that one is returned. Otherwise, a new
 51// one is created and returned.
 52- (NSXMLElement *)elementForApp:(NSString *)appID;
 53
 54// add an incremental amount more of XML based on one KSTicket
 55- (NSXMLElement *)elementFromTicket:(KSTicket *)t;
 56
 57// convenience wrapper for element+attribute creation
 58- (NSXMLElement *)addElement:(NSString *)name withAttribute:(NSString *)attr
 59                 stringValue:(NSString *)value toParent:(NSXMLElement *)parent;
 60
 61// See if the given productID needs to have an <o:ping> element added to
 62// the update request.  |actives_| is used to determine whether this
 63// element is needed, and what the element's attributes should be.
 64- (void)addPingElementForProductID:(NSString *)productID
 65                          toParent:(NSXMLElement *)parent;
 66
 67// Return the complete NSData object for an XML document (e.g. including header)
 68- (NSData *)dataFromDocument;
 69
 70// Returns a dictionary containing NSString key/value pairs for all of the XML
 71// attributes of |node|.
 72- (NSMutableDictionary *)dictionaryWithXMLAttributesForNode:(NSXMLNode *)node;
 73
 74// Given a dictionary of key/value attributes (as NSStrings), returns the
 75// corresponding KSUpdateInfo object. If required keys are missing, nil will
 76// be returned.
 77- (KSUpdateInfo *)updateInfoWithAttributes:(NSDictionary *)attributes;
 78
 79// Returns YES if the specified |url| is safe to fetch; NO otherwise. See the
 80// implementation for more details about what's safe and what's not.
 81- (BOOL)isAllowedURL:(NSURL *)url;
 82
 83@end
 84
 85
 86@implementation KSOmahaServer
 87
 88+ (id)serverWithURL:(NSURL *)url {
 89  return [self serverWithURL:url params:nil];
 90}
 91
 92+ (id)serverWithURL:(NSURL *)url params:(NSDictionary *)params {
 93  return [[[self alloc] initWithURL:url params:params] autorelease];
 94}
 95
 96+ (id)serverWithURL:(NSURL *)url params:(NSDictionary *)params
 97             engine:(KSUpdateEngine *)engine {
 98  return [[[self alloc] initWithURL:url params:params engine:engine]
 99           autorelease];
100}
101
102- (id)initWithURL:(NSURL *)url params:(NSDictionary *)params
103           engine:(KSUpdateEngine *)engine {
104  // First thing, we need to create our params dictionary, which has some
105  // default values that can be overriden by the caller-specified |params|.
106  // The -addEntriesFromDictionary call will replace (override) existing values,
107  // which is what we want.
108  NSMutableDictionary *defaultParams = [[self class] defaultParams];
109  if (params)
110    [defaultParams addEntriesFromDictionary:params];
111
112  if ((self = [super initWithURL:url params:defaultParams engine:engine])) {
113    if (![self isAllowedURL:url]) {
114      // These lines can never be hit in debug unit test builds because debug
115      // builds allow all URLs, so this block could never be hit.
116      GTMLoggerError(@"Denying connection to %@", url);  // COV_NF_LINE
117      [self release];                                    // COV_NF_LINE
118      return nil;                                        // COV_NF_LINE
119    }
120    [self setupActives];
121  }
122
123  return self;
124}
125
126- (void)dealloc {
127  [document_ release];
128  [actives_ release];
129  [super dealloc];
130}
131
132- (NSArray *)requestsForTickets:(NSArray *)tickets {
133  if ([tickets count] == 0)
134    return nil;
135  // make sure they're all for me
136  NSEnumerator *tenum = [tickets objectEnumerator];
137  KSTicket *t = nil;
138  while ((t = [tenum nextObject])) {
139    if (![[self url] isEqual:[t serverURL]]) {
140      GTMLoggerError(@"Tickets found with bad URL");
141      return nil;
142    }
143  }
144  [self createRootAndDocument];
145
146  tenum = [tickets objectEnumerator];
147  while ((t = [tenum nextObject])) {
148    [root_ addChild:[self elementFromTicket:t]];
149  }
150  NSData *data = [self dataFromDocument];
151  NSMutableURLRequest *request =
152    [NSMutableURLRequest requestWithURL:[self url]];
153  [request setHTTPMethod:@"POST"];
154  [request setHTTPBody:data];
155
156  GTMLoggerInfo(@"request: %@", [self prettyPrintResponse:nil data:data]);
157
158  // return an array of the one item
159  NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
160  [array addObject:request];
161  return array;
162}
163
164// response can be nil; we never look at it.
165- (NSArray *)updateInfosForResponse:(NSURLResponse *)response
166                               data:(NSData *)data
167                      outOfBandData:(NSDictionary **)oob {
168  if (data == nil)
169    return nil;
170
171  GTMLoggerInfo(@"response: %@", [self prettyPrintResponse:nil data:data]);
172
173  // No out-of-band data until we find some.
174  if (oob) *oob = nil;
175
176  NSError *error = nil;
177  NSXMLDocument *doc = [[[NSXMLDocument alloc]
178                         initWithData:data
179                              options:0
180                                error:&error]
181                          autorelease];
182  if (error != nil) {
183    GTMLoggerError(@"XML error %@ when parsing response", error);
184    return nil;
185  }
186
187  NSArray *apps = [doc nodesForXPath:@".//gupdate/app" error:&error];
188  if (error != nil) {
189    GTMLoggerError(@"XML error %@ when looking for .//gupdate/app",  // COV_NF_LINE
190                   error);
191    return nil;  // COV_NF_LINE
192  }
193
194  // Look for <daystart elapsed_seconds="300" />, an optional return value.
195  // Return an out-of-band dictionary if it exists (and the caller wants it).
196  NSArray *daystarts = [doc nodesForXPath:@".//gupdate/daystart" error:&error];
197    // Pick off one and get its attribute.
198  if ([daystarts count] > 0) {
199    NSXMLNode *daystartNode = [daystarts objectAtIndex:0];
200    NSMutableDictionary *attributes =
201      [self dictionaryWithXMLAttributesForNode:daystartNode];
202    NSString *elapsedSecondsString =
203      [attributes objectForKey:@"elapsed_seconds"];
204    secondsSinceMidnight_ = [elapsedSecondsString intValue];
205
206    if (oob) {
207      NSDictionary *oobData =
208        [NSDictionary
209          dictionaryWithObject:[NSNumber numberWithInt:secondsSinceMidnight_]
210                        forKey:KSOmahaServerSecondsSinceMidnightKey];
211      *oob = oobData;
212    }
213  }
214
215  // The array of update infos that we will return
216  NSMutableArray *updateInfos = [NSMutableArray array];
217  NSEnumerator *aenum = [apps objectEnumerator];
218  NSXMLElement *element = nil;
219
220  // Iterate through each <app ...> ... </app> element
221  while ((element = [aenum nextObject])) {
222
223    // First, make sure the status of the <app> is "ok"
224    NSArray *statusNodes = [element nodesForXPath:@"./@status" error:&error];
225    if (error != nil || [statusNodes count] == 0) {
226      GTMLoggerError(@"No statuses for %@, error=%@", element, error);
227      continue;
228    }
229    NSString *status = [[statusNodes objectAtIndex:0] stringValue];
230    if (![status isEqualToString:@"ok"]) {
231      GTMLoggerError(@"Bad status for %@", element);
232      continue;
233    }
234
235    // Now, collect all the attributes of "./updatecheck"
236    // (<app><updatecheck ...></updatecheck></app>) into a mutable dictionary.
237    // We'll make sure we got all the required attributes later.
238    NSArray *updateCheckNodes = [element nodesForXPath:@"./updatecheck"
239                                                 error:&error];
240    if (error != nil || [updateCheckNodes count] == 0) {
241      GTMLoggerError(@"Failed to get updatecheck from %@, error=%@",
242                     element, error);
243      continue;
244    }
245    NSXMLNode *updatecheckNode = [updateCheckNodes objectAtIndex:0];
246
247    NSMutableDictionary *attributes =
248      [self dictionaryWithXMLAttributesForNode:updatecheckNode];
249    GTMLoggerInfo(@"Attributes from XMLNode %@ = %@",
250                  updatecheckNode, [attributes description]);
251
252    // Pick up the product ID from the appid attribute
253    // (<app appid="..."></app>)
254    NSArray *appIDNodes = [element nodesForXPath:@"./@appid" error:&error];
255    if (error != nil || [appIDNodes count] == 0) {
256      GTMLoggerError(@"Failed to get appid from %@, error=%@",
257                     element, error);
258      continue;
259    }
260    NSXMLNode *appID = [appIDNodes objectAtIndex:0];
261    NSString *productID = [appID stringValue];
262
263    // Notify the delegate about the ping successes before possibly
264    // bailing out for a "noupdate" status.
265    id delegate = [[self engine] delegate];
266    if (delegate) {
267      NSArray *pingNodes = [element nodesForXPath:@"./ping/@status"
268                                            error:&error];
269      if ([pingNodes count] > 0) {
270        NSXMLNode *pingNode = [pingNodes objectAtIndex:0];
271        if ([[pingNode stringValue] isEqualToString:@"ok"]) {
272          NSDate *biasedNow =
273            [NSDate dateWithTimeIntervalSinceNow:-secondsSinceMidnight_];
274          if ([delegate respondsToSelector:
275                          @selector(engine:serverData:forProductID:withKey:)]) {
276            if ([actives_ didSendRollCallForProductID:productID]) {
277              [delegate engine:[self engine]
278                    serverData:biasedNow
279                  forProductID:productID
280                       withKey:kUpdateEngineLastRollCallPingDate];
281            }
282            if ([actives_ didSendActiveForProductID:productID]) {
283              [delegate engine:[self engine]
284                    serverData:biasedNow
285                  forProductID:productID
286                       withKey:kUpdateEngineLastActivePingDate];
287            }
288          }
289        }
290      }
291    }
292
293    // Make sure the "status" attribute of the "updatecheck" node is "ok"
294    if (![[attributes objectForKey:@"status"] isEqualToString:@"ok"]) {
295      continue;
296    }
297
298    // Stuff the appid (product ID) into our attributes dictionary
299    [attributes setObject:productID forKey:kServerProductID];
300
301    // Build a KSUpdateInfo from the XML attributes and add that to our
302    // array of update infos to return.
303    KSUpdateInfo *updateInfo = [self updateInfoWithAttributes:attributes];
304    if (updateInfo) {
305      [updateInfos addObject:updateInfo];
306    } else {
307      GTMLoggerError(@"can't create KSUpdateInfo from element %@", element);
308    }
309  }
310
311  return updateInfos;
312}
313
314- (NSString *)prettyPrintResponse:(NSURLResponse *)response
315                             data:(NSData *)data {
316  NSError *error = nil;
317  NSXMLDocument *doc = [[[NSXMLDocument alloc]
318                         initWithData:data
319                         options:0
320                         error:&error]
321                        autorelease];
322  if (error != nil) {
323    GTMLoggerError(@"XML error %@ when printing response", error);
324    return nil;
325  }
326
327  NSData *d2 = [doc XMLDataWithOptions:NSXMLNodePrettyPrint];
328  NSString *str = [[[NSString alloc] initWithData:d2
329                                         encoding:NSUTF8StringEncoding]
330                   autorelease];
331  return str;
332}
333
334- (NSURLRequest *)requestForStats:(KSStatsCollection *)stats {
335  if ([stats count] == 0)
336    return nil;
337
338  [self buildDocumentForStats:stats];
339  NSData *data = [self dataFromDocument];
340
341  NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[self url]];
342  [req setHTTPMethod:@"POST"];
343  [req setHTTPBody:data];
344
345  return req;
346}
347
348@end
349
350
351@implementation KSOmahaServer (Private)
352
353+ (NSMutableDictionary *)defaultParams {
354  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
355  [dict setObject:@"10" forKey:kUpdateEngineOSVersion];
356  [dict setObject:@"0" forKey:kUpdateEngineIsMachine];
357  return dict;
358}
359
360- (void)setupActives {
361
362  // Populate the actives with all the stored dates, which is a dictionary
363  // keyed by productID containing the interesting dates.
364  NSDictionary *params = [self params];
365  NSDictionary *activesInfos =
366    [params objectForKey:kUpdateEngineProductActiveInfoKey];
367  NSEnumerator *activeKeyEnumerator = [activesInfos keyEnumerator];
368  NSString *productID;
369
370  actives_ = [[KSClientActives alloc] init];
371  while ((productID = [activeKeyEnumerator nextObject])) {
372    NSDictionary *perProductInfo = [activesInfos objectForKey:productID];
373    NSDate *lastRollCall =
374      [perProductInfo objectForKey:kUpdateEngineLastRollCallPingDate];
375    NSDate *lastPing =
376      [perProductInfo objectForKey:kUpdateEngineLastActivePingDate];
377    NSDate *lastActive =
378      [perProductInfo objectForKey:kUpdateEngineLastActiveDate];
379    [actives_ setLastRollCallPing:lastRollCall
380                   lastActivePing:lastPing
381                       lastActive:lastActive
382                     forProductID:productID];
383  }
384}
385
386// The resulting XML document will look something like the following:
387/*
388
389 <?xml version="1.0" encoding="UTF-8"?>
390 <o:gupdate xmlns:o="http://www.google.com/update2/request"
391            version="UpdateEngine-0.1.4.0"
392            protocol="2.0"
393            ismachine="0">
394
395   <o:os version="MacOSX" platform="mac" sp="10"></o:os>
396
397   <o:app appid="com.google.test2">
398     <o:ping a="1" r="1"></o:ping>
399     <o:event errorcode="1"></o:event>
400   </o:app>
401
402   <o:app appid="com.google.test3">
403     <o:ping a="-1" r="1"></o:ping>
404   </o:app>
405
406   <o:kstat baz="-1" foo="1" bar="1"></o:kstat>
407
408 </o:gupdate>
409
410 */
411- (void)buildDocumentForStats:(KSStatsCollection *)stats {
412  if (stats == nil) return;
413
414  [self createRootAndDocument];
415
416  NSXMLElement *kstat = [NSXMLNode elementWithName:@"o:kstat"];
417
418  NSDictionary *statsDict = [stats statsDictionary];
419  NSEnumerator *statEnumerator = [statsDict keyEnumerator];
420  NSString *statKey = nil;
421
422  // Iterate all of the stats in |statsDict|
423  //   for each stat that is a per-product stat, add it to a <o:app> element
424  //   for each stat that is machine-wide, add it to the <o:kstat> element
425  while ((statKey = [statEnumerator nextObject])) {
426    if (KSIsProductStatKey(statKey)) {
427      // Handle the per-product stats
428      NSString *product = KSProductFromStatKey(statKey);
429      NSString *stat = KSStatFromStatKey(statKey);
430
431      NSXMLElement *app = [self elementForApp:product];
432
433      if ([stat isEqualToString:kStatInstallRC]) {
434        // If this per-product stat is "kStatInstallRC", then add an event
435        // element to record the errorcode (this is basically sending up the
436        // return value from this app's update's return code).
437        NSString *value = [[stats numberForStat:statKey] stringValue];
438        // Build the per-app XML element for this app
439        [self addElement:@"o:event"
440           withAttribute:@"errorcode"
441             stringValue:value
442                toParent:app];
443      }
444
445      // Add this app element to the root node if necessary
446      if ([app parent] == nil)
447        [root_ addChild:app];
448
449    } else {
450      // Handle the machine-wide stat by adding an attribute to the
451      // <o:kstat> element
452
453      NSString *statValue = [[stats numberForStat:statKey] stringValue];
454      NSXMLNode *statAttribute = [NSXMLNode attributeWithName:statKey
455                                                  stringValue:statValue];
456      [kstat addAttribute:statAttribute];
457    }
458  }
459
460  [root_ addChild:kstat];
461}
462
463// Helper to return the version of our bundle as an NSString.
464- (NSString *)bundleVersion {
465  NSBundle *bundle = [NSBundle bundleForClass:[self class]];
466  if (bundle) {
467    NSDictionary *info = [bundle infoDictionary];
468    NSString *version = [info objectForKey:(NSString*)kCFBundleVersionKey];
469    return version;
470  }
471  GTMLoggerDebug(@"No bundle version found");  // COV_NF_LINE
472  // found nothing!
473  return @"0";  // COV_NF_LINE
474}
475
476/*
477<?xml version="1.0" encoding="UTF-8"?>
478<o:gupdate xmlns:o="http://www.google.com/update2/request" version="UpdateEngine-1.0"
479    protocol="2.0"
480    ismachine="1">
481  <o:os version="MacOSX" platform="mac" sp="10.5.2_x86"></o:os>
482
483    ...right here: filled in via -elementFromTicket, lower...
484
485</o:gupdate>
486*/
487- (void)createRootAndDocument {
488  if (document_) {
489    [document_ release];  // owner of root_
490    root_ = nil;
491    document_ = nil;
492  }
493  root_ = [NSXMLNode elementWithName:@"o:gupdate"];  // root_ owned by document_
494  NSString *xmlns = @"http://www.google.com/update2/request";
495  [root_ addAttribute:[NSXMLNode attributeWithName:@"xmlns:o"
496                                       stringValue:xmlns]];
497
498  NSString *identity = [[self params] objectForKey:kUpdateEngineIdentity];
499  if (!identity) identity = @"UpdateEngine";
500  NSString *version = [NSString stringWithFormat:@"%@-%@",
501                                identity, [self bundleVersion]];
502  [root_ addAttribute:[NSXMLNode attributeWithName:@"version"
503                                       stringValue:version]];
504  [root_ addAttribute:[NSXMLNode attributeWithName:@"protocol"
505                                       stringValue:@"2.0"]];
506
507  NSString *ismachine = [[self params] objectForKey:kUpdateEngineIsMachine];
508  [root_ addAttribute:[NSXMLNode attributeWithName:@"ismachine"
509                                 stringValue:ismachine]];
510  // 'tag' is optional; it may be nil.
511  NSString *tag = [[self params] objectForKey:kUpdateEngineUpdateCheckTag];
512  if (tag) [root_ addAttribute:[NSXMLNode attributeWithName:@"tag"
513                                                stringValue:tag]];
514
515  NSXMLElement *child = [NSXMLNode elementWithName:@"o:os"];
516  [child addAttribute:[NSXMLNode attributeWithName:@"version"
517                                       stringValue:@"MacOSX"]];
518  [child addAttribute:[NSXMLNode attributeWithName:@"platform"
519                                       stringValue:@"mac"]];
520  // Omaha convention: OS version is "5" (XP) or "6" (Vista)
521  // "sp" (service pack) for OS minor version (e.g. 1, 2, etc).
522  // UpdateEngine convention: OS version is "MacOSX"
523  // "sp" is full version number with an arch appended (e.g. "10.5.2_x86")
524  NSString *sp = [[self params] objectForKey:kUpdateEngineOSVersion];
525  [child addAttribute:[NSXMLNode attributeWithName:@"sp"
526                                       stringValue:sp]];
527  [root_ addChild:child];
528
529  document_ = [[NSXMLDocument alloc] initWithRootElement:root_];
530}
531
532- (NSXMLElement *)elementForApp:(NSString *)appID {
533  if (appID == nil) return nil;
534
535  // We first check to see if we can find a child element of |root_| which has
536  // "appid" == |appID|, if we find one, we return that one. Otherwise, we
537  // create a new app element with the requested appid.
538
539  NSError *error = nil;
540  NSString *xpath = [NSString stringWithFormat:@".//app[@appid='%@']", appID];
541  NSArray *nodes = [root_ nodesForXPath:xpath error:&error];
542  if (error) {
543    GTMLoggerError(@"XPath ('%@') failed with error %@", xpath, error);  // COV_NF_LINE
544  }
545
546  NSXMLElement *app = nil;
547  if ([nodes count] > 0) {
548    app = [nodes objectAtIndex:0];
549  }
550
551  if (app == nil) {
552    app = [NSXMLNode elementWithName:@"o:app"];
553    [app addAttribute:[NSXMLNode attributeWithName:@"appid" stringValue:appID]];
554  }
555  return app;
556}
557
558- (void)addPingElementForProductID:(NSString *)productID
559                          toParent:(NSXMLElement *)parent {
560  int rollcallDays = [actives_ rollCallDaysForProductID:productID];
561  int activeDays = [actives_ activeDaysForProductID:productID];
562
563  if (rollcallDays == kKSClientActivesDontReport &&
564      activeDays == kKSClientActivesDontReport) {
565    // No ping.
566    return;
567  }
568
569  NSXMLElement *ping = [NSXMLNode elementWithName:@"o:ping"];
570  // The "r=#" attribute is the number of days since the last roll-call
571  // ping.
572  if (rollcallDays != kKSClientActivesDontReport) {
573    NSString *rollcallString = [NSString stringWithFormat:@"%d", rollcallDays];
574    [ping addAttribute:[NSXMLNode attributeWithName:@"r"
575                                        stringValue:rollcallString]];
576    [actives_ sentRollCallForProductID:productID];
577  }
578  // The "a=#" attribute is the number of days since the last active ping.
579  if (activeDays != kKSClientActivesDontReport) {
580    NSString *activeString = [NSString stringWithFormat:@"%d", activeDays];
581    [ping addAttribute:[NSXMLNode attributeWithName:@"a"
582                                       stringValue:activeString]];
583    [actives_ sentActiveForProductID:productID];
584  }
585  [parent addChild:ping];
586}
587
588- (NSXMLElement *)elementFromTicket:(KSTicket *)t {
589  NSXMLElement *el = [self elementForApp:[t productID]];
590  [el addAttribute:[NSXMLNode attributeWithName:@"version"
591                                    stringValue:[t determineVersion]]];
592  [el addAttribute:[NSXMLNode attributeWithName:@"lang" stringValue:@"en-us"]];
593  // Set the "install age", as determined by the ticket's creation date.
594  NSDate *creationDate = [t creationDate];
595  // |creationDate| should be non-nil, but avoid getting a potentially bad
596  // value from a double-sized return from a nil message send, just in case.
597  if (creationDate) {
598    NSTimeInterval ticketAge = [creationDate timeIntervalSinceNow];
599    // Don't use creation dates from the future.
600    if (ticketAge < 0) {
601      const int kSecondsPerDay = 24 * 60 * 60;
602      int ageInDays = (int)(ticketAge / -kSecondsPerDay);
603      NSString *age = [NSString stringWithFormat:@"%d", ageInDays];
604      [el addAttribute:[NSXMLNode attributeWithName:@"installage"
605                                        stringValue:age]];
606    }
607  }
608  if ([[[self params] objectForKey:kUpdateEngineUserInitiated] boolValue]) {
609    [el addAttribute:[NSXMLNode attributeWithName:@"installsource"
610                                      stringValue:@"ondemandupdate"]];
611  }
612
613  NSString *tag = [t determineTag];
614  if (tag)
615    [el addAttribute:[NSXMLNode attributeWithName:@"tag"
616                                      stringValue:tag]];
617  NSString *brand = [t determineBrand];
618  if (!brand) brand = DEFAULT_BRAND_CODE;
619  [el addAttribute:[NSXMLNode attributeWithName:@"brand"
620                                    stringValue:brand]];
621
622  // Adds o:ping element.
623  [self addPingElementForProductID:[t productID]
624                          toParent:el];
625
626  NSString *ttTokenString = nil;
627  NSString *ttTokenValue = [t trustedTesterToken];
628  if (ttTokenValue)
629    ttTokenString = @"tttoken";
630
631  [self addElement:@"o:updatecheck" withAttribute:ttTokenString
632       stringValue:ttTokenValue toParent:el];
633
634  return el;
635}
636
637- (NSXMLElement *)addElement:(NSString *)name withAttribute:(NSString *)attr
638                 stringValue:(NSString *)value toParent:(NSXMLElement *)parent {
639  NSXMLElement *child = [NSXMLNode elementWithName:name];
640  if (attr && value)
641    [child addAttribute:[NSXMLNode attributeWithName:attr stringValue:value]];
642  [parent addChild:child];
643  return child;
644}
645
646// Warning: NSData is not a c-string; it is not NULL-terminated.
647- (NSData *)dataFromDocument {
648  NSString *header = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
649  NSData *xml = [document_ XMLDataWithOptions:NSXMLNodePrettyPrint];
650  NSMutableData *data = [NSMutableData dataWithCapacity:([xml length] +
651                                                         [header length])];
652  [data appendData:[header dataUsingEncoding:NSUTF8StringEncoding]];
653  [data appendData:xml];
654  return data;
655}
656
657// Given an NSXMLNode, returns a dictionary containing all of the node's
658// attributes and attribute values as NSStrings.
659- (NSMutableDictionary *)dictionaryWithXMLAttributesForNode:(NSXMLNode *)node {
660  if (node == nil) return nil;
661
662  NSError *error = nil;
663  NSArray *attributes = [node nodesForXPath:@"./@*" error:&error];
664  if ([attributes count] == 0) return nil;
665
666  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
667  NSXMLNode *attr = nil;
668  NSEnumerator *attrEnumerator = [attributes objectEnumerator];
669
670  while ((attr = [attrEnumerator nextObject])) {
671    [dict setObject:[attr stringValue]
672             forKey:[attr name]];
673  }
674
675  return dict;
676}
677
678// Given a dictionary of key/value pair attributes, returns the corresponding
679// KSUpdateInfo object. We basically do this by converting some of the values in
680// |attributes| to more appropriate types (e.g., an NSString representing a URL
681// into an actual NSURL), and verifying that required attributes are present.
682//
683// We also ensure that all required attributes have the known, required keys.
684// For example, we don't just make sure that the Omaha server returned a
685// "codebase" attribute, but we change the key to be kServerCodebaseURL, and we
686// change the value to be an actual NSURL.
687//
688// If any errors occur, return nil.
689- (KSUpdateInfo *)updateInfoWithAttributes:(NSDictionary *)attributes {
690  if (attributes == nil) return nil;
691
692  NSMutableDictionary *updateInfo = [[attributes mutableCopy] autorelease];
693
694  // Transform "codebase" => kServerCodebaseURL, and make the value an NSURL
695  NSString *codebase = [updateInfo objectForKey:@"codebase"];
696  if (codebase) {
697    NSURL *url = [NSURL URLWithString:codebase];
698    if (url) {
699      [updateInfo removeObjectForKey:@"codebase"];
700      [updateInfo setObject:url forKey:kServerCodebaseURL];
701    }
702  }
703
704  // Transform "size" => kServerCodeSize, and make it an NSNumber (int)
705  int size = [[updateInfo objectForKey:@"size"] intValue];
706  [updateInfo removeObjectForKey:@"size"];
707  [updateInfo setObject:[NSNumber numberWithInt:size]
708                 forKey:kServerCodeSize];
709
710  // Transform "hash" => kServerCodeHash
711  NSString *hash = [updateInfo objectForKey:@"hash"];
712  if (hash) {
713    [updateInfo removeObjectForKey:@"hash"];
714    [updateInfo setObject:hash forKey:kServerCodeHash];
715  }
716
717  // The next couple of keys are our extensions to the Omaha server
718  // protocol, via "Pair" entries in the Update Rule in the product
719  // configuration file.  They are capitalized like the other rules
720  // in the configuration - CamelCapWithLeadingCapitalLetterKthx.
721
722  // Transform "Prompt" => kServerPromptUser, and make it an NSNumber (bool)
723  NSString *prompt = [updateInfo objectForKey:@"Prompt"];
724  if (prompt) {
725    BOOL shouldPrompt = ([prompt isEqualToString:@"yes"] ||
726                         [prompt isEqualToString:@"true"]);
727    // Must cast BOOL to int because DO is going to transform the underlying
728    // CFBoolean into an NSNumber (int) during transit anyway, and we want the
729    // dict to still be "equal" after DO transfer.
730    [updateInfo setObject:[NSNumber numberWithInt:(int)shouldPrompt]
731                   forKey:kServerPromptUser];
732  }
733
734  // Transform "RequireReboot" => kServerRequireReboot, and make it
735  // an NSNumber (bool)
736  NSString *reboot = [updateInfo objectForKey:@"RequireReboot"];
737  if (reboot) {
738    BOOL requireReboot = ([reboot isEqualToString:@"yes"] ||
739                           [reboot isEqualToString:@"true"]);
740    // Must cast BOOL to int because DO is going to transform the underlying
741    // CFBoolean into an NSNumber (int) during transit anyway, and we want the
742    // dict to still be "equal" after DO transfer.
743    [updateInfo setObject:[NSNumber numberWithInt:(int)requireReboot]
744                   forKey:kServerRequireReboot];
745  }
746
747  // Transform "MoreInfo" => kServerMoreInfoURLString.
748  NSString *moreinfo = [updateInfo objectForKey:@"MoreInfo"];
749  if (moreinfo) {
750    [updateInfo setObject:moreinfo forKey:kServerMoreInfoURLString];
751  }
752
753  // Transform "LocalizationBundle" => kServerLocalizationBundle
754  NSString *localizationBundle =
755    [updateInfo objectForKey:@"LocalizationBundle"];
756  if (localizationBundle) {
757    [updateInfo setObject:localizationBundle
758                   forKey:kServerLocalizationBundle];
759  }
760
761  // Transform "DisplayVersion" => kServerDisplayVersion
762  NSString *displayVersion = [updateInfo objectForKey:@"DisplayVersion"];
763  if (displayVersion) {
764    [updateInfo setObject:displayVersion
765                   forKey:kServerDisplayVersion];
766  }
767
768  // Transform "Version" => kServerVersion
769  NSString *version = [updateInfo objectForKey:@"Version"];
770  if (version) {
771    [updateInfo setObject:version
772                   forKey:kServerVersion];
773  }
774
775  // Verify that all required keys are present
776  NSArray *requiredKeys = [NSArray arrayWithObjects:
777                            kServerProductID, kServerCodebaseURL,
778                            kServerCodeSize, kServerCodeHash, nil];
779  NSEnumerator *keyEnumerator = [requiredKeys objectEnumerator];
780  NSString *key = nil;
781  while ((key = [keyEnumerator nextObject])) {
782    if ([updateInfo objectForKey:key] == nil) {
783      GTMLoggerError(@"Missing required key '%@' in %@", key, updateInfo);
784      return nil;
785    }
786  }
787
788  return updateInfo;
789}
790
791// Allow URLs that match any of the following:
792// - Allow everything in DEBUG builds (includes unit tests)
793// - Uses a file: scheme
794// - Uses https: scheme to a certain google.com subdomain
795- (BOOL)isAllowedURL:(NSURL *)url {
796  if (url == nil) return NO;
797
798#ifdef DEBUG
799  // Anything goes, debug style.
800  return YES;
801#endif
802
803  // Disallow anything but https: urls
804  if (![[url scheme] isEqualToString:@"https"])
805    return NO;
806
807  // If supplied, only allow URLs to the allowed subdomains.
808  NSArray *allowedSubdomains =
809    [[self params] objectForKey:kUpdateEngineAllowedSubdomains];
810  if (!allowedSubdomains)
811    return YES;
812
813  NSString *host = [@"." stringByAppendingString:[url host]];
814  NSPredicate *filter = [NSPredicate predicateWithFormat:
815                         @"%@ ENDSWITH SELF", host];
816  NSArray *matches = [allowedSubdomains filteredArrayUsingPredicate:filter];
817
818  if ([matches count] > 0)
819    return YES;
820
821  // No match, so deny.
822  return NO;
823}
824
825@end