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