DIRECTORY Arpa, ArpaExtras USING [MyAddress], ArpaName, ArpaNameCache, ArpaNameQuery USING [ARR, CNameRR, MailForwardRecord, MxRR, NsRR, PtrRR, Query, QType, Reply, Seconds, SoaRR, SourceOfAuthRecord], ArpaNameLogSupport, ArpaNameSupport, ArpaRouter USING [GetHops, Hops, unreachable], ArpaUDP USING [Milliseconds], Commander USING [CommandProc, Register], IO USING [STREAM, Flush, PutRope], Process USING [Detach], Rope USING [ROPE, Cat, Find, IsEmpty, Length, Substr], RopeList USING [Append], ViewerEvents USING [EventProc, RegisterEventProc], ViewerIO USING [CreateViewerStreams, GetViewerFromStream, Viewer]; ArpaNameImpl: CEDAR MONITOR IMPORTS ArpaExtras, ArpaNameCache, ArpaNameLogSupport, ArpaNameSupport, ArpaNameQuery, ArpaRouter, Commander, IO, Process, Rope, RopeList, ViewerEvents, ViewerIO EXPORTS ArpaName = BEGIN OPEN ArpaName; ROPE: TYPE = Rope.ROPE; arpanetRootServers: LIST OF Arpa.Address _ LIST[[10,0,0,51], [26,0,0,73],[10,0,0,52],[26,3,0,103],[128,20,1,2],[192,5,25,82]]; localRootServers: LIST OF Arpa.Address _ LIST[[13,2,16,8], [13,1,100,208]]; localDefaultDomain: ROPE _ "parc.Xerox.COM"; myAddress: Arpa.Address = ArpaExtras.MyAddress[]; downEntryTTL: ArpaNameQuery.Seconds _ 1777; -- ~ 0.5 hour, s/b < sick mail host retry time downServerTTL: ArpaNameQuery.Seconds _ 1777; -- ~ 0.5 hour bogusTTL: ArpaNameQuery.Seconds _ 86400; -- 1 day mxDefaultTTL: ArpaNameQuery.Seconds _ 86400; -- 1 day upperLimitTTL: ArpaNameQuery.Seconds _ 1222222; -- approx. 2 weeks, 2's for noticeability lowerLimitTTL: ArpaNameQuery.Seconds _ 1222; -- approx. 20 mins GetLocalRootServers: PUBLIC PROC RETURNS[addrs: LIST OF Arpa.Address] = {RETURN[localRootServers]}; SetLocalRootServers: PUBLIC PROC[addrs: LIST OF Arpa.Address] = { IF addrs # NIL THEN localRootServers _ addrs}; GetArpanetRootServers: PUBLIC PROC RETURNS[addrs: LIST OF Arpa.Address] = {RETURN[arpanetRootServers]}; SetArpanetRootServers: PUBLIC PROC[addrs: LIST OF Arpa.Address] = { IF addrs # NIL THEN arpanetRootServers _ addrs}; GetDefaultDomain: PUBLIC PROC RETURNS[domain: ROPE] = {RETURN[localDefaultDomain]}; SetDefaultDomain: PUBLIC PROC[domain: ROPE] = { IF ~Rope.IsEmpty[domain] THEN localDefaultDomain _ domain}; MyName: PUBLIC PROC RETURNS[name: ROPE_NIL] = { [name, , ] _ AddressToName[myAddress]; IF name = NIL THEN RETURN[ArpaNameSupport.AddressToRope[myAddress]]; }; NameToAddress: PUBLIC PROC [name: Rope.ROPE, resolv: BOOL _ FALSE] RETURNS [addr: Arpa.Address _ Arpa.nullAddress, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { addrs: LIST OF Arpa.Address _ NIL; IF Rope.IsEmpty[name] THEN RETURN[Arpa.nullAddress, bogus, myAddress]; [addrs, status, source] _ NameToAddressList[name, resolv]; IF addrs = NIL THEN RETURN[Arpa.nullAddress, status, source]; RETURN[addrs.first, status, source]; }; NameToAddressList: PUBLIC PROC [name: Rope.ROPE, resolv: BOOL _ FALSE] RETURNS [addrs: LIST OF Arpa.Address _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { found: BOOL; DoLookups: PROC[name: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, newName: ROPE _ NIL, addrs: LIST OF Arpa.Address _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { cName: ROPE _ NIL; [found, addrs, source] _ NameToAddressLookup[name, resolv]; IF found AND addrs # NIL THEN {status _ ok; RETURN}; [found, cName, source] _ AliasToNameLookup[name, resolv]; IF found AND ~Rope.IsEmpty[cName] THEN newName _ cName ELSE newName _ name; [found, addrs, source] _ NameToAddressLookup[newName, resolv]; IF found AND addrs # NIL THEN {status _ ok; RETURN}; [found, source] _ NameToBogusLookup[newName, resolv]; IF found THEN {status _ bogus; RETURN}; [found, source] _ NameToDownLookup[newName]; IF found THEN {status _ down; RETURN}; }; IF Rope.IsEmpty[name] THEN RETURN[NIL, bogus, myAddress]; IF ArpaNameSupport.IsAddressRope[name] THEN { -- It's an address already address: Arpa.Address _ ArpaNameSupport.RopeToAddress[name]; IF address # Arpa.nullAddress THEN RETURN[LIST[ArpaNameSupport.RopeToAddress[name]], ok, myAddress] ELSE RETURN[NIL, bogus, myAddress]; }; name _ DefaultDomain[name]; [found, name, addrs, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[addrs, status, source]; DoQuery[name, a, resolv]; [found, name, addrs, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[addrs, status, source]; [found,, source] _ NameToZoneLookup[name, resolv]; IF found THEN RETURN[NIL, other, source]; [found,, source] _ NameToMXLookup[name, resolv]; IF found THEN RETURN[NIL, other, source]; RETURN[NIL, other, myAddress]; }; NameToMailHostList: PUBLIC PROC [name: ROPE, resolv: BOOL _ FALSE] RETURNS [hosts: LIST OF ROPE _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { found: BOOL; DoLookups: PROC[name: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, newName: ROPE _ NIL, hosts: LIST OF ROPE _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { cName: ROPE _ NIL; [found, hosts, source] _ NameToMXLookup[name, resolv]; IF found AND hosts # NIL THEN {status _ ok; RETURN}; [found, cName, source] _ AliasToNameLookup[name, resolv]; IF found AND ~Rope.IsEmpty[cName] THEN name _ newName ELSE newName _ name; [found, hosts, source] _ NameToMXLookup[name, resolv]; IF found AND hosts # NIL THEN {status _ ok; RETURN}; [found, source] _ NameToBogusLookup[name, resolv]; IF found THEN {status _ bogus; RETURN}; [found, source] _ NameToDownLookup[name]; IF found THEN {status _ down; RETURN}; }; IF Rope.IsEmpty[name] THEN RETURN[NIL, bogus, myAddress]; name _ DefaultDomain[name]; [found, name, hosts, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[hosts, status, source]; IF ~ArpaNameCache.FetchZone[name].found THEN DoQuery[name, ns, resolv]; [found, name, hosts, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[hosts, status, source]; -- in case bogus or down, avoid extra query DoQuery[name, mx, resolv]; [found, name, hosts, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[hosts, status, source]; [found,, source] _ NameToZoneLookup[name, resolv]; IF found THEN RETURN[NIL, other, source]; [found,, source] _ NameToAddressLookup[name, resolv]; IF found THEN RETURN[NIL, other, source]; RETURN[NIL, other, myAddress]; }; AddressToName: PUBLIC PROC [addr: Arpa.Address, resolv: BOOL _ FALSE] RETURNS [name: Rope.ROPE_NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { addrRope: ROPE _ ArpaNameSupport.AddressToRope[addr]; found: BOOL; DoLookups: PROC[rope: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, name: ROPE _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { [found, name, source] _ AddressToNameLookup[rope, resolv]; IF found AND name # NIL THEN {status _ ok; RETURN}; [found, source] _ AddressToBogusLookup[rope, resolv]; IF found THEN {status _ bogus; RETURN}; [found, source] _ AddressToDownLookup[rope]; IF found THEN {status _ down; RETURN}; }; [found, name, status, source] _ DoLookups[addrRope, resolv]; IF found THEN { IF name = NIL AND status # bogus THEN name _ addrRope; RETURN[name, status, source]; }; DoQuery[addrRope, ptr, resolv]; [found, name, status, source] _ DoLookups[addrRope, resolv]; IF found THEN { IF name = NIL AND status # bogus THEN name _ addrRope; RETURN[name, status, source]; }; RETURN[name, other, myAddress]; }; AddressRopeToName: PUBLIC PROC [addr: ROPE, resolv: BOOL _ FALSE] RETURNS [name: ROPE, status: ReplyStatus _ down, source: Arpa.Address_ myAddress] = { address: Arpa.Address; IF Rope.IsEmpty[addr] THEN RETURN[NIL, bogus, source]; address _ ArpaNameSupport.RopeToAddress[addr]; IF address = Arpa.nullAddress THEN RETURN[NIL, bogus, source]; [name, status, source] _ AddressToName[address, resolv]; IF status = down OR status = other THEN name _ addr; }; AliasToName: PUBLIC PROC [alias: Rope.ROPE, resolv: BOOL _ FALSE] RETURNS [name: Rope.ROPE_ NIL, status: ReplyStatus _ down, source: Arpa.Address _ Arpa.nullAddress] = { found: BOOL; DoLookups: PROC[alias: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, name: ROPE _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { [found, name, source] _ AliasToNameLookup[alias, resolv]; IF found AND name # NIL THEN RETURN[found, name, ok, source]; [found, source] _ NameToBogusLookup[alias, resolv]; IF found THEN RETURN[found, NIL, bogus, source]; [found, source] _ NameToDownLookup[alias]; IF found THEN RETURN[found, NIL, down, source]; }; IF Rope.IsEmpty[alias] THEN RETURN[NIL, bogus, myAddress]; alias _ DefaultDomain[alias]; [found, name, status, source] _ DoLookups[alias, resolv]; IF found THEN RETURN[name, status, source]; DoQuery[alias, cName, resolv]; [found, name, status, source] _ DoLookups[alias, resolv]; IF found THEN RETURN[name, status, source]; [found,, source] _ NameToZoneLookup[alias, resolv]; IF found THEN RETURN[alias, other, source]; [found,, source] _ NameToAddressLookup[alias, resolv]; IF found THEN RETURN[alias, other, source]; [found,, source] _ NameToMXLookup[alias, resolv]; IF found THEN RETURN[alias, other, source]; RETURN[alias, other, myAddress]; }; DomainServers: PUBLIC PROC [name: Rope.ROPE, resolv: BOOL _ FALSE] RETURNS [servers: LIST OF Rope.ROPE_ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { found: BOOL; DoLookups: PROC[name: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, servers: LIST OF ROPE _ NIL, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { [found, servers, source] _ NameToZoneLookup[name, resolv]; IF found AND servers # NIL THEN {status _ ok; RETURN}; [found, source] _ NameToBogusLookup[name, resolv]; IF found THEN {status _ bogus; RETURN}; [found, source] _ NameToDownLookup[name]; IF found THEN {status _ down; RETURN}; }; IF Rope.IsEmpty[name] THEN RETURN[NIL, bogus, myAddress]; IF NoDots[name] THEN { status _ AliasToName[name, resolv].status; IF status = ok OR status = other THEN name _ GetDefaultDomain[]; }; [found, servers, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[servers, status, source]; DoQuery[name, ns, resolv]; [found, servers, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[servers, status, source]; name _ StripHead[name].tail; IF Rope.IsEmpty[name] THEN RETURN[NIL, bogus, myAddress]; [found, servers, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[servers, status, source]; DoQuery[name, ns, resolv]; [found, servers, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[servers, status, source]; RETURN[NIL, other, myAddress]; }; DomainInfo: PUBLIC PROC [name: ROPE, resolv: BOOL _ FALSE] RETURNS [domainContact: ROPE, primaryServer: ROPE, status: ReplyStatus, source: Arpa.Address _ myAddress] = { found: BOOL; DoLookups: PROC[name: ROPE, resolv: BOOL _ FALSE] RETURNS[found: BOOL _ FALSE, domainContact: ROPE, primaryServer: ROPE, status: ReplyStatus _ down, source: Arpa.Address _ myAddress] = { [found, domainContact, primaryServer, source] _ NameToZoneInfoLookup[name, resolv]; IF found AND domainContact # NIL THEN {status _ ok; RETURN}; [found, source] _ NameToBogusLookup[name, resolv]; IF found THEN {status _ bogus; RETURN}; [found, source] _ NameToDownLookup[name]; IF found THEN {status _ down; RETURN}; }; IF Rope.IsEmpty[name] THEN RETURN[NIL, NIL, bogus, myAddress]; IF NoDots[name] THEN { status _ AliasToName[name, resolv].status; IF status = ok OR status = other THEN name _ GetDefaultDomain[]; }; [found, domainContact, primaryServer, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[domainContact, primaryServer, status, source]; DoQuery[name, soa, resolv]; [found, domainContact, primaryServer, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[domainContact, primaryServer, status, source]; name _ StripHead[name].tail; IF Rope.IsEmpty[name] THEN RETURN[NIL, NIL, bogus, myAddress]; [found, domainContact, primaryServer, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[domainContact, primaryServer, status, source]; DoQuery[name, soa, resolv]; [found, domainContact, primaryServer, status, source] _ DoLookups[name, resolv]; IF found THEN RETURN[domainContact, primaryServer, status, source]; RETURN[NIL, NIL, other, myAddress]; }; ARec: TYPE = RECORD[ loaded: BOOL _ FALSE, name: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, addresses: LIST OF Arpa.Address _ NIL ]; NSRec: TYPE = RECORD[ loaded: BOOL _ FALSE, name: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, servers: LIST OF ROPE _ NIL ]; AliasRec: TYPE = RECORD[ loaded: BOOL _ FALSE, name: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, canonicalName: ROPE _ NIL ]; PtrRec: TYPE = RECORD[ loaded: BOOL _ FALSE, address: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, names: LIST OF ROPE _ NIL ]; SoaRec: TYPE = RECORD[ loaded: BOOL _ FALSE, name: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, soa: ArpaNameQuery.SourceOfAuthRecord ]; MXRec: TYPE = RECORD[ loaded: BOOL _ FALSE, name: ROPE _ NIL, ttl: ArpaNameQuery.Seconds, mxList: LIST OF ArpaNameQuery.MailForwardRecord _ NIL ]; DoQuery: PROC[query: ROPE, type: ArpaNameQuery.QType, resolv: BOOL _ FALSE, serverHint: Arpa.Address _ Arpa.nullAddress] = { reply: ArpaNameQuery.Reply; queryRope: ROPE; aRec: ARec; nsRec: NSRec; mxRec: MXRec; aliasRec: AliasRec; soaRec: SoaRec; ptrRec: PtrRec; source: Arpa.Address _ myAddress; IF Rope.IsEmpty[query] THEN RETURN; IF type = ptr THEN { queryRope _ ArpaNameSupport.AddressRopeToQueryRope[query]; IF Rope.IsEmpty[queryRope] THEN RETURN} ELSE queryRope _ query; reply _ SendQuery[queryRope, type, resolv, serverHint]; IF reply = NIL THEN { SELECT type FROM a, ns, cName, soa, mx => { entry: ArpaNameCache.DownNameEntry _ ArpaNameCache.UpdateDownName[query, myAddress, downEntryTTL, TRUE]; ArpaNameLogSupport.DownNameSummaryLine[entry, TRUE,,Log]; }; ptr => { entry: ArpaNameCache.DownAddressEntry _ ArpaNameCache.UpdateDownAddressRope[query, myAddress, downEntryTTL, TRUE]; ArpaNameLogSupport.DownAddressSummaryLine[entry, TRUE,,Log]; }; ENDCASE; RETURN; }; source _ reply.source; SELECT reply.hdr.rcode FROM ok => NULL; nameError => { IF reply.hdr.authoritative THEN { SELECT type FROM a, ns, cName, soa, mx => { entry: ArpaNameCache.BogusNameEntry _ ArpaNameCache.UpdateBogusName[query, source, bogusTTL, TRUE]; ArpaNameLogSupport.BogusNameSummaryLine[entry, TRUE,,Log]; }; ptr => { entry: ArpaNameCache.BogusAddressEntry _ ArpaNameCache.UpdateBogusAddressRope[query, source, bogusTTL, TRUE]; ArpaNameLogSupport.BogusAddressSummaryLine[entry, TRUE,,Log]; }; ENDCASE; RETURN}; }; ENDCASE => { SELECT type FROM a, ns, cName, soa, mx => { entry: ArpaNameCache.DownNameEntry _ ArpaNameCache.UpdateDownName[query, source, downEntryTTL, TRUE]; ArpaNameLogSupport.DownNameSummaryLine[entry, TRUE,,Log]; }; ptr => { entry: ArpaNameCache.DownAddressEntry _ ArpaNameCache.UpdateDownAddressRope[query, source, downEntryTTL, TRUE]; ArpaNameLogSupport.DownAddressSummaryLine[entry, TRUE,,Log]; }; ENDCASE; RETURN; }; IF reply.anCount = 0 AND reply.nsCount = 0 AND type = mx AND reply.hdr.authoritative AND ~ArpaNameCache.FetchZone[query].found THEN { mxRec.loaded _ TRUE; mxRec.name _ query; mxRec.ttl _ mxDefaultTTL; mxRec.mxList _ AppendUniqueMXRec[[preference: 0, host: query], mxRec.mxList]; }; FOR i: INT IN [0..reply.anCount) DO WITH reply.answers[i] SELECT FROM rr: ArpaNameQuery.ARR => IF rr.class = in THEN { aRec.loaded _ TRUE; aRec.name _ rr.name; aRec.ttl _ LimitTTL[rr.ttl]; aRec.addresses _ ArpaNameSupport.AppendUniqueAddress[rr.address, aRec.addresses]; }; rr: ArpaNameQuery.PtrRR => IF rr.class = in THEN { ptrRec.loaded _ TRUE; ptrRec.address _ query; ptrRec.ttl _ LimitTTL[rr.ttl]; ptrRec.names _ RopeList.Append[ptrRec.names, LIST[rr.ptrRope]]; }; rr: ArpaNameQuery.CNameRR => IF rr.class = in THEN { aliasRec.loaded _ TRUE; aliasRec.name _ rr.name; aliasRec.ttl _ LimitTTL[rr.ttl]; aliasRec.canonicalName _ rr.cNameRope; }; rr: ArpaNameQuery.NsRR => IF rr.class = in THEN { nsRec.loaded _ TRUE; nsRec.name _ rr.name; nsRec.ttl _ LimitTTL[rr.ttl]; nsRec.servers _ RopeList.Append[nsRec.servers, LIST[rr.serverRope]]; }; rr: ArpaNameQuery.SoaRR => IF rr.class = in THEN { soaRec.loaded _ TRUE; soaRec.name _ rr.name; soaRec.ttl _LimitTTL[rr.ttl]; soaRec.soa _ rr.soaRec; }; rr: ArpaNameQuery.MxRR => IF rr.class = in THEN { mxRec.loaded _ TRUE; mxRec.name _ rr.name; mxRec.ttl _ LimitTTL[rr.ttl]; mxRec.mxList _ AppendUniqueMXRec[rr.mxRec, mxRec.mxList]; }; ENDCASE; ENDLOOP; IF aRec.loaded THEN { entry: ArpaNameCache.NameEntry; aRec.addresses _ SortAddresses[aRec.addresses]; entry _ ArpaNameCache.UpdateName[aRec.name, source, aRec.ttl, reply.hdr.authoritative, aRec.addresses]; ArpaNameLogSupport.NameSummaryLine[entry, TRUE,,Log]; }; IF nsRec.loaded THEN { entry: ArpaNameCache.ZoneEntry _ ArpaNameCache.UpdateZone[nsRec.name, source, nsRec.ttl, reply.hdr.authoritative, nsRec.servers]; ArpaNameLogSupport.ZoneSummaryLine[entry, TRUE,,Log]; }; IF mxRec.loaded THEN { entry: ArpaNameCache.MXEntry; mxRec.mxList _ SortMXList[mxRec.mxList]; entry _ ArpaNameCache.UpdateMX[mxRec.name, source, mxRec.ttl, reply.hdr.authoritative, mxRec.mxList]; ArpaNameLogSupport.MXSummaryLine[entry, TRUE,,Log]; }; IF aliasRec.loaded THEN { entry: ArpaNameCache.AliasEntry _ ArpaNameCache.UpdateAlias[aliasRec.name, source, aliasRec.ttl, reply.hdr.authoritative, aliasRec.canonicalName]; ArpaNameLogSupport.AliasSummaryLine[entry, TRUE,,Log]; }; IF soaRec.loaded THEN { entry: ArpaNameCache.SoaEntry _ ArpaNameCache.UpdateSoa[soaRec.name, source, soaRec.ttl, reply.hdr.authoritative, NEW[ArpaNameQuery.SourceOfAuthRecord _ soaRec.soa]]; ArpaNameLogSupport.SoaSummaryLine[entry, TRUE,,Log]; }; IF ptrRec.loaded THEN { entry: ArpaNameCache.AddressEntry _ ArpaNameCache.UpdateAddressRope[ptrRec.address, source, ptrRec.ttl, reply.hdr.authoritative, ptrRec.names]; ArpaNameLogSupport.AddressSummaryLine[entry, TRUE,,Log]; }; }; LimitTTL: PROC[in: ArpaNameQuery.Seconds] RETURNS[out: ArpaNameQuery.Seconds] = { out _ in; IF in > upperLimitTTL THEN out _ upperLimitTTL; IF in < lowerLimitTTL THEN out _ lowerLimitTTL; }; SendQuery: PROC[rope: ROPE, type: ArpaNameQuery.QType, resolv: BOOL _ FALSE, serverHint: Arpa.Address _ Arpa.nullAddress] RETURNS[reply: ArpaNameQuery.Reply] = { servers: LIST OF Arpa.Address; nServers: CARD; baseTimeout: ARRAY [1..3] OF ArpaUDP.Milliseconds = [4000, 8000, 16000]; downServer: ArpaNameCache.DownServerEntry _ NIL; IF Rope.IsEmpty[rope] THEN RETURN[NIL]; [servers, nServers] _ FindServers[rope, resolv, serverHint]; IF nServers = 0 OR servers = NIL THEN RETURN[NIL]; FOR list: LIST OF Arpa.Address _ servers, list.rest UNTIL list = NIL DO reply _ ArpaNameQuery.Query[ server: list.first, query: rope, type: type, class: in, recurDesired: ~resolv, timeout: 3000, -- 3 seconds retry: 0]; IF reply # NIL THEN { IF reply.hdr.rcode = nameError OR reply.hdr.rcode = ok THEN RETURN}; ENDLOOP; FOR list: LIST OF Arpa.Address _ servers, list.rest UNTIL list = NIL DO FOR retry: INT IN [1..3] DO reply _ ArpaNameQuery.Query[ server: list.first, query: rope, type: type, class: in, recurDesired: ~resolv, timeout: baseTimeout[retry]/nServers, retry: 0]; IF reply # NIL THEN { IF reply.hdr.rcode = nameError OR reply.hdr.rcode = ok THEN RETURN}; ENDLOOP; downServer _ ArpaNameCache.UpdateDownServer[list.first, myAddress, downServerTTL, TRUE]; ArpaNameLogSupport.DownServerSummaryLine[downServer, TRUE,, Log]; ENDLOOP; }; FindServers: PROC [rope: ROPE, resolv: BOOL _ FALSE, serverHint: Arpa.Address _ Arpa.nullAddress] RETURNS[answers: LIST OF Arpa.Address_ NIL, nServers: CARD _ 0] = { servers: LIST OF Arpa.Address _ IF resolv THEN arpanetRootServers ELSE localRootServers; IF serverHint # Arpa.nullAddress THEN answers _ ArpaNameSupport.AppendUniqueAddress[serverHint, answers]; FOR list: LIST OF Arpa.Address _ servers, list.rest UNTIL list = NIL DO server: Arpa.Address _ list.first; IF ~ArpaNameCache.FetchDownServer[server, FALSE].found THEN answers _ ArpaNameSupport.AppendUniqueAddress[server, answers]; ENDLOOP; nServers _ ArpaNameSupport.CountAddresses[answers]; RETURN[answers, nServers]; }; SortAddresses: PUBLIC PROC [list: LIST OF Arpa.Address] RETURNS[result: LIST OF Arpa.Address] = { rest: LIST OF Arpa.Address; bestAddress: Arpa.Address _ BestAddress[list]; IF list = NIL THEN RETURN[NIL]; result _ LIST[bestAddress]; rest _ RemoveAddress[list, bestAddress]; UNTIL rest = NIL DO bestAddress _ BestAddress[rest]; result _ ArpaNameSupport.AppendUniqueAddress[bestAddress, result]; rest _ RemoveAddress[rest, bestAddress]; ENDLOOP; }; RemoveAddress: PROC [list: LIST OF Arpa.Address, target: Arpa.Address] RETURNS[result: LIST OF Arpa.Address] = { z: LIST OF Arpa.Address _ NIL; result _ NIL; UNTIL list = NIL DO IF list.first # target THEN {IF result = NIL THEN {result _ CONS[list.first, NIL]; z _ result} ELSE {z.rest _ CONS[list.first, z.rest]; z _ z.rest}}; list _ list.rest; ENDLOOP; }; BestAddress: PUBLIC PROC [addresses: LIST OF Arpa.Address] RETURNS [bestAddress: Arpa.Address] = { bestHops: ArpaRouter.Hops _ ArpaRouter.unreachable; bestAddress _ Arpa.nullAddress; FOR list: LIST OF Arpa.Address _ addresses, list.rest UNTIL list = NIL DO hops: ArpaRouter.Hops _ ArpaRouter.GetHops[list.first]; IF hops < bestHops THEN {bestHops _ hops; bestAddress _ list.first}; ENDLOOP; IF bestAddress = Arpa.nullAddress THEN bestAddress _ addresses.first; RETURN[bestAddress]; }; SortMXList: PROC[list: LIST OF ArpaNameQuery.MailForwardRecord] RETURNS[result: LIST OF ArpaNameQuery.MailForwardRecord] = { rest: LIST OF ArpaNameQuery.MailForwardRecord; bestMXRec: ArpaNameQuery.MailForwardRecord _ BestMXRec[list]; IF list = NIL THEN RETURN[NIL]; result _ LIST[bestMXRec]; rest _ RemoveMXRec[list, bestMXRec]; UNTIL rest = NIL DO bestMXRec _ BestMXRec[rest]; result _ AppendUniqueMXRec[bestMXRec, result]; rest _ RemoveMXRec[rest, bestMXRec]; ENDLOOP; RETURN[result]; }; BestMXRec: PUBLIC PROC [in: LIST OF ArpaNameQuery.MailForwardRecord] RETURNS [bestMXRec: ArpaNameQuery.MailForwardRecord] = { bestPreference: INT _ INT.LAST; FOR list: LIST OF ArpaNameQuery.MailForwardRecord _ in, list.rest UNTIL list = NIL DO IF list.first.preference < bestPreference THEN {bestPreference _ list.first.preference; bestMXRec _ list.first}; ENDLOOP; RETURN[bestMXRec]; }; RemoveMXRec: PROC [list: LIST OF ArpaNameQuery.MailForwardRecord, target: ArpaNameQuery.MailForwardRecord] RETURNS[result: LIST OF ArpaNameQuery.MailForwardRecord] = { z: LIST OF ArpaNameQuery.MailForwardRecord _ NIL; result _ NIL; UNTIL list = NIL DO IF list.first # target THEN {IF result = NIL THEN {result _ CONS[list.first, NIL]; z _ result} ELSE {z.rest _ CONS[list.first, z.rest]; z _ z.rest}}; list _ list.rest; ENDLOOP; }; AppendUniqueMXRec: PUBLIC PROC [mxRec: ArpaNameQuery.MailForwardRecord, list: LIST OF ArpaNameQuery.MailForwardRecord _ NIL] RETURNS [new: LIST OF ArpaNameQuery.MailForwardRecord _ NIL] = { IF list = NIL THEN RETURN[LIST[mxRec]]; new _ list; DO IF list.first = mxRec THEN RETURN; IF list.rest = NIL THEN { list.rest _ LIST[mxRec]; RETURN; }; list _ list.rest; ENDLOOP; }; NameToBogusLookup: PROC [name: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.BogusNameEntry _ NIL; [found, expired, entry] _ ArpaNameCache.FetchBogusName[name, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, a, resolv, source]]}; -- attempt refresh RETURN[found, source]; }; NameToDownLookup: PROC [name: ROPE] RETURNS [found: BOOL _ FALSE, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.DownNameEntry; [found, expired, entry] _ ArpaNameCache.FetchDownName[name, FALSE]; IF entry # NIL THEN source _ entry.source; RETURN[found, source]; }; AddressToBogusLookup: PROC [rope: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.BogusAddressEntry _ NIL; [found, expired, entry] _ ArpaNameCache.FetchBogusAddressRope[rope, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownAddressRope[rope, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[rope, ptr, resolv, source]]};-- attempt refresh RETURN[found, source]; }; AddressToDownLookup: PROC [rope: ROPE] RETURNS [found: BOOL _ FALSE, source: Arpa.Address _ myAddress] = { expired: BOOL; entry: ArpaNameCache.DownAddressEntry; [found, expired, entry] _ ArpaNameCache.FetchDownAddressRope[rope, FALSE]; IF entry # NIL THEN source _ entry.source; RETURN[found, source]; }; NameToAddressLookup: PROC [name: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, addrs: LIST OF Arpa.Address _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.NameEntry; [found, expired, entry] _ ArpaNameCache.FetchName[name, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, a, resolv, source]]};-- attempt refresh IF found THEN { addrs _ entry.addresses; IF addrs # NIL THEN RETURN[found, addrs, source]}; RETURN[FALSE, NIL, source]; }; AddressToNameLookup: PROC [rope: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, name: ROPE _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.AddressEntry; [found, expired, entry] _ ArpaNameCache.FetchAddressRope[rope, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownAddressRope[rope, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[rope, ptr, resolv, source]]};-- attempt refresh IF found THEN { name _ entry.names.first; IF name # NIL THEN RETURN[found, name, source]}; RETURN[FALSE, NIL, source]; }; NameToMXLookup: PROC [name: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, hosts: LIST OF ROPE _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { mxList: LIST OF ArpaNameQuery.MailForwardRecord _ NIL; entry: ArpaNameCache.MXEntry; expired: BOOL; [found, expired, entry] _ ArpaNameCache.FetchMX[name, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, mx, resolv, source]]};-- attempt refresh IF found THEN { mxList _ entry.mxList; hosts _ HostsFromMXList[mxList]; IF hosts # NIL THEN RETURN[found, hosts, source]; }; RETURN[FALSE, NIL, source]; }; HostsFromMXList: PROC [mxList: LIST OF ArpaNameQuery.MailForwardRecord] RETURNS [hosts: LIST OF ROPE _ NIL] ~ { IF mxList = NIL THEN RETURN; FOR list: LIST OF ArpaNameQuery.MailForwardRecord _ mxList, list.rest UNTIL list = NIL DO hosts _ RopeList.Append[hosts, LIST[list.first.host]]; ENDLOOP; }; NameToZoneLookup: PROC [name: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, servers: LIST OF ROPE _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { entry: ArpaNameCache.ZoneEntry; expired: BOOL; [found, expired, entry] _ ArpaNameCache.FetchZone[name, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, ns, resolv, source]]}; -- attempt refresh IF found THEN { servers _ entry.servers; IF name # NIL THEN RETURN[found, servers, source]}; RETURN[FALSE, NIL, source]; }; NameToZoneInfoLookup: PROC [name: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, contact: ROPE _ NIL, primaryServer: ROPE _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { entry: ArpaNameCache.SoaEntry; expired: BOOL; [found, expired, entry] _ ArpaNameCache.FetchSoa[name, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, soa, resolv, source]]};-- attempt refresh IF found THEN { contact _ entry.soaRef.domainContact; primaryServer _ entry.soaRef.primaryServer; IF ~Rope.IsEmpty[contact] THEN { IF NoAt[contact] THEN { head, tail: ROPE; [head, tail] _ StripHead[contact]; contact _ Rope.Cat[head, "@", tail]; }; RETURN[found, contact, primaryServer, source] }; }; RETURN[FALSE, NIL, NIL, source]; }; AliasToNameLookup: PROC [alias: ROPE, resolv: BOOL _ FALSE, acceptOld: BOOL _ TRUE] RETURNS [found: BOOL _ FALSE, name: ROPE _ NIL, source: Arpa.Address _ Arpa.nullAddress] = { expired: BOOL; entry: ArpaNameCache.AliasEntry; [found, expired, entry] _ ArpaNameCache.FetchAlias[alias, acceptOld]; IF entry # NIL THEN source _ entry.source; IF expired AND ~ArpaNameCache.FetchDownName[name, FALSE].found THEN TRUSTED {Process.Detach[FORK DoQuery[name, cName, resolv, source]]};-- attempt refresh IF found THEN { name _ entry.canonicalName; IF name # NIL THEN RETURN[found, name, source]}; RETURN[FALSE, NIL, source]; }; StripHead: PROC[inRope: ROPE] RETURNS [head: Rope.ROPE_NIL, tail: ROPE_NIL] = { dot, length: INT _ 0; dot _ Rope.Find[inRope, ".",, FALSE]; IF dot <=0 THEN RETURN[NIL]; length _ Rope.Length[inRope]; tail _ Rope.Substr[inRope, dot+1, length-dot-1]; head _ Rope.Substr[inRope, 0, dot]; }; NoDots: PROC[name: ROPE] RETURNS[BOOL] = {RETURN[Rope.Find[name, ".",, FALSE] = -1]}; NoAt: PROC[name: ROPE] RETURNS[BOOL] = {RETURN[Rope.Find[name, "@",, FALSE] = -1]}; DefaultDomain: PROC[name: ROPE] RETURNS[new: ROPE] = { IF NoDots[name] THEN new _ Rope.Cat[name, ".", GetDefaultDomain[]] ELSE new _ name;}; Log: PROC [rope: ROPE] ~ { IF log # NIL THEN { IO.PutRope[log, rope]; IO.Flush[log]}; }; log: IO.STREAM _ NIL; DestroyProc: ViewerEvents.EventProc = { log _ NIL; }; StartWatcher: Commander.CommandProc = { out: IO.STREAM; viewer: ViewerIO.Viewer; IF log #NIL THEN RETURN[msg: "ArpaNameWatcher already started."]; out _ ViewerIO.CreateViewerStreams [ name: "ArpaNameWatcher.log", backingFile: "ArpaNameWatcher.log", editedStream: FALSE].out; viewer _ ViewerIO.GetViewerFromStream[out]; [] _ ViewerEvents.RegisterEventProc[proc: DestroyProc, event: destroy, filter: viewer]; log _ out; }; Commander.Register["ArpaNameWatcher", StartWatcher, "Start up a typescript to watch ArpaName operations."]; END. €ArpaNameImpl.mesa Copyright c 1987 by Xerox Corporation. All rights reserved. John Larson, November 1, 1987 7:01:11 pm PST Bootstrap configuration data Setable from ArpaName interface Arpanet root servers: SRI-NIC.ARPA = [10.0.0.51] [26.0.0.73] C.ISI.EDU = [10.0.0.52] A.ISI.EDU = [26.3.0.103] BRL-AOS.ARPA = [128.20.1.2], [192.5.25.82] Local root servers: parcvax.Xerox.COM = [13.2.16.8] [10.1.0.32] palain.parc.Xerox.COM =[13.1.100.208] Want to avoid bogus mx records being loaded.. see comments in DoQuery Grrr.... lack of support for caching negative responses is one of the problems with the current domain system. We must cache implict MX records to avoid the extra MX query load on external servers, but interpreting ambiguous responses such as below is a problem. Mail clients should always do a Zone query before MX to avoid bogus mx entries being loaded here (which would cause mail accidently sent to a zone to be put in the sick queue rather than being properly rejected). Find working name server Try harder to find a working name server. Timeouts are based on an exponential backoff scheme as a function of the number of servers. Max total timeout ~ 30 secs. 1 server: 4, 8, 16 secs 2 servers: 2, 4, 8 secs 3 servers: 1, 2, 5 secs [viewer: ViewerClasses.Viewer, event: ViewerEvents.ViewerEvent, before: BOOL] RETURNS [abort: BOOL _ FALSE] [cmd: Commander.Handle] RETURNS [result: REF ANY _ NIL, msg: ROPE _ NIL] Κ(Y˜šœ™Icodešœ Οmœ1™J˜Jšœ,‘ ˜5Jšœ.‘ ˜7J˜Jšœ0‘)˜YJšœ.‘˜AJ˜Icode2šŸœžœžœžœžœžœžœ˜dš Ÿœžœžœžœžœ˜ALšžœ žœžœ˜.—LšŸœžœžœžœžœžœžœ˜hš Ÿœžœžœžœžœ˜CLšžœ žœžœ˜0L˜—Lš Ÿœžœžœžœ žœžœ˜SšŸœžœžœ žœ˜/Lšžœžœ˜;—J˜J˜š Ÿœžœžœžœžœžœ˜/Lšœ&˜&Lšžœžœžœžœ+˜DLšœ˜L˜—J˜š Ÿ œžœžœ žœ žœžœ˜CJšžœj˜qJšœžœžœžœ˜"Jšžœžœžœ%˜FJšœ:˜:Jšžœ žœžœžœ$˜>Jšžœ˜$J˜J˜J˜—š Ÿœžœžœ žœ žœžœ˜GJšžœ žœžœžœC˜mJšœžœ˜ J˜š Ÿ œžœžœžœžœ˜2Jšžœžœžœ žœžœ žœžœžœC˜–Jšœžœžœ˜Jšœ;˜;Jš žœžœ žœžœžœ˜4Jšœ9˜9Jšžœžœžœ˜6Jšžœ˜Jšœ>˜>Jš žœžœ žœžœžœ˜4Jšœ5˜5Jšžœžœžœ˜(Jšœ,˜,Jšžœžœžœ˜'Jšœ˜—J˜Jšžœžœžœžœ˜9šžœ%žœ‘˜HJšœ<˜Lšœ8˜8Lšžœžœžœ ˜4Lšœ˜L˜—š Ÿ œžœžœžœ žœžœ˜BLšžœ žœžœJ˜gJšœžœ˜ J˜š Ÿ œžœžœžœžœ˜3Jš žœžœžœžœžœC˜pJšœ9˜9Jš žœžœžœžœžœ˜=Jšœ3˜3Jšžœžœžœžœ˜1Jšœ*˜*Jšžœžœžœžœ˜0Jšœ˜—J˜Lšžœžœžœžœ˜:Jšœ˜J˜Jšœ9˜9Jšžœžœžœ˜,J˜Jšœ ˜ Jšœ˜Jšœ9˜9Jšžœžœžœ˜,J˜Jšœ3˜3Jšžœžœžœ˜,Jšœ6˜6Jšžœžœžœ˜,Jšœ1˜1Jšžœžœžœ˜,Jšžœ˜ Lšœ˜L˜L˜—š Ÿ œžœžœ žœ žœžœ˜CLš žœ žœžœžœžœC˜kJšœžœ˜ J˜š Ÿ œžœžœžœžœ˜2Jšžœžœžœ žœžœžœžœC˜{Jšœ:˜:Jš žœžœ žœžœžœ˜6Jšœ2˜2Jšžœžœžœ˜(Jšœ)˜)Jšžœžœžœ˜'Jšœ˜—J˜Lšžœžœžœžœ˜9J˜šžœžœ˜Lšœ*˜*Lšžœ žœžœ˜@Lšœ˜L˜—Jšœ;˜;Jšžœžœžœ˜/J˜Jšœ˜J˜Jšœ;˜;Jšžœžœžœ˜/J˜Lšœ˜Lšžœžœžœžœ˜9J˜Jšœ;˜;Jšžœžœžœ˜/J˜Jšœ˜J˜Jšœ;˜;Jšžœžœžœ˜/L˜Jšžœžœ˜L˜Lšœ˜L˜L˜L˜—š Ÿ œžœžœžœ žœžœ˜;Lšžœžœžœ<˜mJšœžœ˜ L˜š Ÿ œžœžœžœžœ˜2Lš žœžœžœžœžœC˜ˆJšœS˜SJš žœžœžœžœžœ˜L˜šžœžœ˜Lšœ*˜*Lšžœ žœžœ˜@Lšœ˜—J˜JšœP˜PJšžœžœžœ0˜DJ˜Jšœ˜J˜JšœP˜PJšžœžœžœ0˜DJ˜Lšœ˜Lš žœžœžœžœžœ˜>J˜JšœP˜PJšžœžœžœ0˜DJ˜Jšœ˜J˜JšœP˜PJšžœžœžœ0˜DJ˜Jšžœžœžœ˜#L˜Lšœ˜L˜L˜—šœžœžœ˜Lšœžœžœ˜Lšœžœžœ˜Lšœ˜Lšœ žœžœž˜%Lšœ˜—šœžœžœ˜Lšœžœžœ˜Lšœžœžœ˜Lšœ˜Lšœ žœžœžœž˜Lšœ˜—šœ žœžœ˜Lšœžœžœ˜Lšœžœžœ˜Lšœ˜Lšœžœž˜Lšœ˜—šœžœžœ˜Lšœžœžœ˜Lšœ žœžœ˜Lšœ˜Lšœžœžœž˜Lšœ˜—šœžœžœ˜Lšœžœžœ˜Lšœžœžœ˜Lšœ˜Lšœ%˜%Lšœ˜—šœžœžœ˜Lšœžœžœ˜Lšœžœžœ˜Lšœ˜Lšœžœžœ#ž˜5Lšœ˜L˜L˜—J˜š Ÿœžœžœ%žœžœ2˜|Jšœ˜Jšœ ž˜Jšœ ˜ Jšœ ˜ Jšœ ˜ Lšœ˜Lšœ˜Jšœ˜Lšœ!˜!L˜Jšžœžœžœ˜#J˜šžœ žœ˜Jšœ:˜:Jšžœžœžœ˜'—šžœ˜J˜—J˜Lšœ7˜7šžœ žœžœ˜šžœž˜šœ˜Jšœ%˜%Jšœ=žœ˜DJšœ.žœ˜9J˜—šœ˜Jšœlžœ˜sJšœ1žœ˜Jš žœžœžœžœžœ˜Jšœ žœ ˜Jšœ$˜$šžœžœž˜Jšœ˜Jšœ.˜.Jšœ$˜$Jšžœ˜—Jšžœ ˜Jšœ˜J˜J˜—š Ÿ œžœžœžœžœ"˜ELšžœ1˜8Jšœžœžœžœ˜š žœžœžœ1žœžœž˜Ušžœ(žœ˜/JšœA˜AJšžœ˜——Lšžœ ˜Lšœ˜L˜L˜—šŸ œžœžœžœK˜kLšžœ žœžœ&˜=Kšœžœžœ#žœ˜1Kšœ žœ˜ šžœžœž˜šžœž˜Kš œžœ žœžœ žœ žœ˜BKšžœ žœ#˜6——K˜Kšžœ˜Kšœ˜J˜—š Ÿœžœžœ0žœžœ#žœ˜|Jšžœžœžœ#žœ˜@Jš žœžœžœžœžœ ˜'Jšœ ˜ šž˜Jšžœžœžœ˜"Jš žœ žœžœžœ žœ˜=Jšœ˜Jšžœ˜ J˜——J˜J˜šŸœžœžœ žœžœ žœžœ˜RJšžœ žœžœ.˜JJšœ žœ˜Jšœ&žœ˜*JšœH˜HJšžœ žœžœ˜*šžœ žœ$žœžœ˜DJšžœžœ%‘˜S—Jšžœ˜Jšœ˜J˜J˜—šŸœžœžœ˜#Jšžœ žœžœ.˜JJšœ žœ˜Jšœ#˜#Jšœ<žœ˜CJšžœ žœžœ˜*Jšžœ˜Jšœ˜J˜J˜—šŸœžœžœ žœžœ žœžœ˜UJšžœ žœžœ.˜JJšœ žœ˜Jšœ)žœ˜-JšœO˜OJšžœ žœžœ˜*šžœ žœ+žœžœ˜KJšžœžœ&‘˜T—Jšžœ˜Jšœ˜J˜J˜—šŸœžœžœ˜&Jšžœ žœžœ'˜CJšœ žœ˜Jšœ&˜&JšœCžœ˜JJšžœ žœžœ˜*Jšžœ˜Jšœ˜J˜J˜J˜—šŸœžœžœ žœžœ žœžœ˜UJš žœ žœžœ žœžœžœ.˜mJšœ žœ˜Jšœ˜JšœC˜CJšžœ žœžœ˜*šžœ žœ$žœžœ˜DJšžœžœ$‘˜R—šžœžœ˜Jšœ˜Jšžœ žœžœžœ˜2—Jšžœžœžœ ˜Jšœ˜J˜J˜—šŸœžœžœ žœžœ žœžœ˜TJš žœ žœžœžœžœ.˜\Jšœ žœ˜Jšœ"˜"JšœJ˜JJšžœ žœžœ˜*šžœ žœ+žœžœ˜KJšžœžœ&‘˜T—šžœžœ˜Jšœ˜Jšžœžœžœžœ˜0—Jšžœžœžœ ˜Jšœ˜J˜—šŸœžœžœ žœžœ žœžœ˜OJšžœ žœžœ žœžœžœžœ.˜eJšœžœžœ#žœ˜6Jšœ˜Jšœ žœ˜JšœA˜AJšžœ žœžœ˜*šžœ žœ$žœžœ˜DJšžœžœ%‘˜S—šžœžœ˜Jšœ˜Jšœ ˜ Jšžœ žœžœžœ˜1J˜—Jšžœžœžœ ˜Jšœ˜J˜J˜—šŸœžœ žœžœ"˜HKš žœ žœžœžœžœ˜'Kšžœ žœžœžœ˜š žœžœžœ5žœžœž˜YJšœžœ˜7Jšžœ˜—K˜J˜J˜—šŸœžœžœ žœžœ žœžœ˜QJšžœ žœžœ žœžœžœžœ.˜gJšœ˜Jšœ žœ˜JšœC˜CJšžœ žœžœ˜*šžœ žœ#žœžœ˜DJšžœžœ&‘˜T—šžœžœ˜Jšœ˜Jšžœžœžœžœ˜3—Jšžœžœžœ ˜Jšœ˜J˜J˜—šŸœžœžœ žœžœ žœžœ˜UJšžœ žœžœ žœžœžœžœ.˜zJšœ˜Jšœ žœ˜JšœB˜BJšžœ žœžœ˜*šžœ žœ$žœžœ˜DJšžœžœ&‘˜T—šžœžœ˜Jšœ&˜&Jšœ,˜,šžœžœ˜ šžœžœ˜Jšœ žœ˜JšœG˜GJšœ˜—Jšžœ'˜-Jšœ˜—J˜—Jšžœžœžœžœ ˜ Jšœ˜J˜J˜—šŸœžœ žœ žœžœ žœžœ˜SJš žœ žœžœžœžœ.˜\Jšœ žœ˜Jšœ ˜ JšœE˜EJšžœ žœžœ˜*šžœ žœ$žœžœ˜DJšžœžœ(‘˜V—šžœžœ˜Jšœ˜Jšžœžœžœžœ˜0—Jšžœžœžœ ˜Jšœ˜J˜—šŸ œžœ žœžœ žœžœžœžœ˜OLšœ žœ˜Lšœžœ˜%Lšžœ žœžœžœ˜Lšœ˜Lšœ0˜0Lšœ#˜#Lšœ˜J˜J˜—JšŸœžœžœžœžœžœžœ ˜UJšŸœžœžœžœžœžœžœ ˜SJ˜š Ÿ œžœžœžœžœ˜6Jšžœžœ.˜BJšžœ˜J˜—šŸœžœžœ˜šžœžœžœ˜Kšžœ˜Kšžœ ˜—Kšœ˜K˜—Jšœžœžœžœ˜J˜•StartOfExpansiono -- [viewer: ViewerClasses.Viewer, event: ViewerEvents.ViewerEvent, before: BOOL] RETURNS [abort: BOOL _ FALSE]šΟb œ˜'KšΠckk™kKšœžœ˜ K˜K˜—J˜šŸ œ˜'Kš œžœ žœžœžœžœžœ™HKšœžœžœ˜Kšœ˜Kšžœžœžœžœ*˜Ašœ$˜$JšœOžœ˜Z—Kšœ+˜+K–s[proc: ViewerEvents.EventProc, event: ViewerEvents.ViewerEvent, filter: REF ANY _ NIL, before: BOOL _ TRUE]šœW˜WKšœ ˜ Jšœ˜J˜J˜—šœk˜kJ˜J˜—Jšžœ˜J˜J˜J˜—J˜—…—w¦₯£