msg_dht_p2p.go (20798B)
1 // This file is part of gnunet-go, a GNUnet-implementation in Golang. 2 // Copyright (C) 2019-2022 Bernd Fix >Y< 3 // 4 // gnunet-go is free software: you can redistribute it and/or modify it 5 // under the terms of the GNU Affero General Public License as published 6 // by the Free Software Foundation, either version 3 of the License, 7 // or (at your option) any later version. 8 // 9 // gnunet-go is distributed in the hope that it will be useful, but 10 // WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 // Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 // 17 // SPDX-License-Identifier: AGPL3.0-or-later 18 19 package message 20 21 import ( 22 "bytes" 23 "crypto/sha512" 24 "encoding/binary" 25 "errors" 26 "fmt" 27 "gnunet/config" 28 "gnunet/crypto" 29 "gnunet/enums" 30 "gnunet/service/dht/blocks" 31 "gnunet/service/dht/path" 32 "gnunet/util" 33 "strings" 34 "time" 35 36 "github.com/bfix/gospel/crypto/ed25519" 37 "github.com/bfix/gospel/logger" 38 ) 39 40 //====================================================================== 41 // DHT-P2P is a next-generation implementation of the R5N DHT. 42 //====================================================================== 43 44 //---------------------------------------------------------------------- 45 // DHT-P2P-GET messages are used to request information from other 46 // peers in the DHT. 47 //---------------------------------------------------------------------- 48 49 // DHTP2PGetMsg wire layout 50 type DHTP2PGetMsg struct { 51 MsgHeader 52 BType enums.BlockType `order:"big"` // content type of the payload 53 Flags uint16 `order:"big"` // processing flags 54 HopCount uint16 `order:"big"` // number of hops so far 55 ReplLevel uint16 `order:"big"` // Replication level 56 RfSize uint16 `order:"big"` // size of result filter 57 PeerFilter *blocks.PeerFilter `` // peer filter to prevent loops 58 Query *crypto.HashCode `` // query hash 59 ResFilter []byte `size:"RfSize"` // result filter 60 XQuery []byte `size:"*"` // extended query 61 } 62 63 // NewDHTP2PGetMsg creates an empty DHT-P2P-Get message 64 func NewDHTP2PGetMsg() *DHTP2PGetMsg { 65 return &DHTP2PGetMsg{ 66 MsgHeader: MsgHeader{208, enums.MSG_DHT_P2P_GET}, 67 BType: enums.BLOCK_TYPE_ANY, // no block type defined 68 Flags: 0, // no flags defined 69 HopCount: 0, // no hops 70 ReplLevel: 0, // no replication level defined 71 RfSize: 0, // no result filter 72 PeerFilter: blocks.NewPeerFilter(), // allocate bloom filter 73 Query: crypto.NewHashCode(nil), // empty Query hash 74 ResFilter: nil, // empty result filter 75 XQuery: nil, // empty XQuery 76 } 77 } 78 79 // Init called after unmarshalling a message to setup internal state 80 func (m *DHTP2PGetMsg) Init() (err error) { return nil } 81 82 // String returns a human-readable representation of the message. 83 func (m *DHTP2PGetMsg) String() string { 84 return fmt.Sprintf("DHTP2PGetMsg{btype=%s,hops=%d,flags=%s}", 85 m.BType, m.HopCount, DHTFlags(m.Flags)) 86 } 87 88 // Update message (forwarding) 89 func (m *DHTP2PGetMsg) Update(pf *blocks.PeerFilter, rf blocks.ResultFilter, hop uint16) *DHTP2PGetMsg { 90 buf := rf.Bytes() 91 ns := uint16(len(buf)) 92 return &DHTP2PGetMsg{ 93 MsgHeader: MsgHeader{m.MsgSize - m.RfSize + ns, enums.MSG_DHT_P2P_GET}, 94 BType: m.BType, 95 Flags: m.Flags, 96 HopCount: hop, 97 ReplLevel: m.ReplLevel, 98 RfSize: ns, 99 PeerFilter: pf.Clone(), 100 Query: m.Query, 101 ResFilter: buf, 102 XQuery: util.Clone(m.XQuery), 103 } 104 } 105 106 //---------------------------------------------------------------------- 107 // DHT-P2P-PUT messages are used by other peers in the DHT to 108 // request block storage. 109 //---------------------------------------------------------------------- 110 111 // DHTP2PPutMsg wire layout 112 type DHTP2PPutMsg struct { 113 MsgHeader 114 BType enums.BlockType `order:"big"` // block type 115 Flags uint16 `order:"big"` // processing flags 116 HopCount uint16 `order:"big"` // message hops 117 ReplLvl uint16 `order:"big"` // replication level 118 PathL uint16 `order:"big"` // path length 119 Expire util.AbsoluteTime `` // expiration date 120 PeerFilter *blocks.PeerFilter `` // peer bloomfilter 121 Key *crypto.HashCode `` // query key to block 122 TruncOrigin *util.PeerID `opt:"(IsUsed)"` // truncated origin (if TRUNCATED flag set) 123 PutPath []*path.Entry `size:"PathL"` // PUT path 124 LastSig *util.PeerSignature `opt:"(IsUsed)"` // signature of last hop (if RECORD_ROUTE flag is set) 125 Block []byte `size:"*"` // block data 126 } 127 128 // NewDHTP2PPutMsg creates an empty new DHTP2PPutMsg 129 func NewDHTP2PPutMsg(block blocks.Block) *DHTP2PPutMsg { 130 // create empty message 131 msg := &DHTP2PPutMsg{ 132 MsgHeader: MsgHeader{216, enums.MSG_DHT_P2P_PUT}, 133 BType: enums.BLOCK_TYPE_ANY, // block type 134 Flags: 0, // processing flags 135 HopCount: 0, // message hops 136 ReplLvl: 0, // replication level 137 PathL: 0, // no PUT path 138 Expire: util.AbsoluteTimeNever(), // expiration date 139 PeerFilter: blocks.NewPeerFilter(), // peer bloom filter 140 Key: crypto.NewHashCode(nil), // query key 141 TruncOrigin: nil, // no truncated path 142 PutPath: make([]*path.Entry, 0), // empty PUT path 143 LastSig: nil, // no signature from last hop 144 Block: nil, // no block data 145 } 146 // initialize with block if available 147 if block != nil { 148 msg.BType = block.Type() 149 msg.HopCount = 0 150 msg.PeerFilter = blocks.NewPeerFilter() 151 msg.ReplLvl = uint16(config.Cfg.GNS.ReplLevel) 152 msg.Expire = block.Expire() 153 msg.Block = block.Bytes() 154 msg.TruncOrigin = nil 155 msg.PutPath = nil 156 msg.LastSig = nil 157 msg.MsgSize += uint16(len(msg.Block)) 158 } 159 return msg 160 } 161 162 // IsUsed returns true if an optional field is used 163 func (m *DHTP2PPutMsg) IsUsed(field string) bool { 164 switch field { 165 case "Origin": 166 return m.Flags&enums.DHT_RO_TRUNCATED != 0 167 case "LastSig": 168 return m.Flags&enums.DHT_RO_RECORD_ROUTE != 0 169 } 170 return false 171 } 172 173 // Init called after unmarshalling a message to setup internal state 174 func (m *DHTP2PPutMsg) Init() (err error) { 175 return nil 176 } 177 178 //---------------------------------------------------------------------- 179 180 // Update message (forwarding) 181 func (m *DHTP2PPutMsg) Update(p *path.Path, pf *blocks.PeerFilter, hop uint16) *DHTP2PPutMsg { 182 msg := NewDHTP2PPutMsg(nil) 183 msg.Flags = m.Flags 184 msg.HopCount = hop 185 msg.PathL = p.NumList 186 msg.Expire = m.Expire 187 msg.PeerFilter = pf 188 msg.Key = m.Key.Clone() 189 msg.TruncOrigin = p.TruncOrigin 190 msg.PutPath = util.Clone(p.List) 191 msg.LastSig = p.LastSig 192 msg.Block = util.Clone(m.Block) 193 msg.SetPath(p) 194 return msg 195 } 196 197 //---------------------------------------------------------------------- 198 // Path handling (get/set path in message) 199 //---------------------------------------------------------------------- 200 201 // Path returns the current path from message 202 func (m *DHTP2PPutMsg) Path(sender *util.PeerID) *path.Path { 203 // create a "real" path list from message data 204 pth := path.NewPath(crypto.Hash(m.Block), m.Expire) 205 pth.Flags = m.Flags 206 207 // return empty path if recording is switched off 208 if pth.Flags&enums.DHT_RO_RECORD_ROUTE == 0 { 209 return pth 210 } 211 212 // handle truncate origin 213 if pth.Flags&enums.DHT_RO_TRUNCATED == 1 { 214 if m.TruncOrigin == nil { 215 logger.Printf(logger.WARN, "[path] truncated but no origin - flag reset") 216 pth.Flags &^= enums.DHT_RO_TRUNCATED 217 } else { 218 pth.TruncOrigin = m.TruncOrigin 219 } 220 } 221 222 // copy path elements 223 pth.List = util.Clone(m.PutPath) 224 pth.NumList = uint16(len(pth.List)) 225 226 // handle last hop signature 227 if m.LastSig == nil { 228 logger.Printf(logger.WARN, "[path] - last hop signature missing - path reset") 229 return path.NewPath(crypto.Hash(m.Block), m.Expire) 230 } 231 pth.LastSig = m.LastSig 232 pth.LastHop = sender 233 return pth 234 } 235 236 // Set path in message; corrects the message size accordingly 237 func (m *DHTP2PPutMsg) SetPath(p *path.Path) { 238 239 // return if recording is switched off (don't touch path) 240 if m.Flags&enums.DHT_RO_RECORD_ROUTE == 0 { 241 return 242 } 243 // compute old path size 244 var pes uint 245 if len(m.PutPath) > 0 { 246 pes = m.PutPath[0].Size() 247 } 248 oldSize := uint(len(m.PutPath)) * pes 249 if m.TruncOrigin != nil { 250 oldSize += m.TruncOrigin.Size() 251 } 252 if m.LastSig != nil { 253 oldSize += m.LastSig.Size() 254 } 255 // if no new path is defined,... 256 if p == nil { 257 // ... remove existing path 258 m.TruncOrigin = nil 259 m.PutPath = nil 260 m.LastSig = nil 261 m.PathL = 0 262 m.Flags &^= enums.DHT_RO_TRUNCATED 263 m.MsgSize -= uint16(oldSize) 264 return 265 } 266 // adjust message size 267 m.MsgSize += uint16(p.Size() - oldSize) 268 269 // transfer path data 270 if p.TruncOrigin != nil { 271 // truncated path 272 m.Flags |= enums.DHT_RO_TRUNCATED 273 m.TruncOrigin = p.TruncOrigin 274 } 275 m.PutPath = util.Clone(p.List) 276 m.PathL = uint16(len(m.PutPath)) 277 if p.LastSig != nil { 278 m.LastSig = p.LastSig 279 } 280 } 281 282 //---------------------------------------------------------------------- 283 284 // String returns a human-readable representation of the message. 285 func (m *DHTP2PPutMsg) String() string { 286 return fmt.Sprintf("DHTP2PPutMsg{btype=%s,hops=%d,flags=%s}", 287 m.BType, m.HopCount, DHTFlags(m.Flags)) 288 } 289 290 //---------------------------------------------------------------------- 291 // DHT-P2P-RESULT messages are used to answer peer requests for 292 // bock retrieval. 293 //---------------------------------------------------------------------- 294 295 // DHTP2PResultMsg wire layout 296 type DHTP2PResultMsg struct { 297 MsgHeader 298 BType enums.BlockType `order:"big"` // Block type of result 299 Reserved uint16 `order:"big"` // Reserved 300 Flags uint16 `order:"big"` // Message flags 301 PutPathL uint16 `order:"big"` // size of PUTPATH field 302 GetPathL uint16 `order:"big"` // size of GETPATH field 303 Expire util.AbsoluteTime `` // expiration date 304 Query *crypto.HashCode `` // Query key for block 305 TruncOrigin *util.PeerID `opt:"(IsUsed)"` // truncated origin (if TRUNCATED flag set) 306 PathList []*path.Entry `size:"(NumPath)"` // PATH 307 LastSig *util.PeerSignature `opt:"(IsUsed)" init:"Init"` // signature of last hop (if RECORD_ROUTE flag is set) 308 Block []byte `size:"*"` // block data 309 } 310 311 // NewDHTP2PResultMsg creates a new empty DHTP2PResultMsg 312 func NewDHTP2PResultMsg() *DHTP2PResultMsg { 313 return &DHTP2PResultMsg{ 314 MsgHeader: MsgHeader{88, enums.MSG_DHT_P2P_RESULT}, 315 BType: enums.BLOCK_TYPE_ANY, // type of returned block 316 TruncOrigin: nil, // no truncated origin 317 PutPathL: 0, // empty putpath 318 GetPathL: 0, // empty getpath 319 PathList: nil, // empty path list (put+get) 320 LastSig: nil, // no recorded route 321 Block: nil, // empty block 322 } 323 } 324 325 // IsUsed returns if an optional field is present 326 func (m *DHTP2PResultMsg) IsUsed(field string) bool { 327 switch field { 328 case "Origin": 329 return m.Flags&enums.DHT_RO_TRUNCATED != 0 330 case "LastSig": 331 return m.Flags&enums.DHT_RO_RECORD_ROUTE != 0 332 } 333 return false 334 } 335 336 // NumPath returns the total number of entries in path 337 func (m *DHTP2PResultMsg) NumPath(field string) uint { 338 return uint(m.GetPathL + m.PutPathL) 339 } 340 341 // Init called after unmarshalling a message to setup internal state 342 func (m *DHTP2PResultMsg) Init() (err error) { 343 return nil 344 } 345 346 //---------------------------------------------------------------------- 347 // Path handling (get/set path in message) 348 //---------------------------------------------------------------------- 349 350 // Path returns the current path from message 351 func (m *DHTP2PResultMsg) Path(sender *util.PeerID) *path.Path { 352 // create a "real" path list from message data 353 pth := path.NewPath(crypto.Hash(m.Block), m.Expire) 354 pth.Flags = m.Flags 355 356 // return empty path if recording is switched off 357 if pth.Flags&enums.DHT_RO_RECORD_ROUTE == 0 { 358 return pth 359 } 360 // handle truncate origin 361 if pth.Flags&enums.DHT_RO_TRUNCATED == 1 { 362 if pth.TruncOrigin == nil { 363 logger.Printf(logger.WARN, "[path] truncated but no origin - flag reset") 364 pth.Flags &^= enums.DHT_RO_TRUNCATED 365 } else { 366 pth.TruncOrigin = m.TruncOrigin 367 } 368 } 369 370 // copy path elements 371 pth.List = util.Clone(m.PathList) 372 pth.NumList = uint16(len(pth.List)) 373 374 // check consistent length values; adjust if mismatched 375 if m.GetPathL+m.PutPathL != pth.NumList { 376 logger.Printf(logger.WARN, "[path] Inconsistent PATH length -- adjusting...") 377 if sp := pth.NumList - m.PutPathL; sp > 0 { 378 pth.SplitPos = sp 379 } else { 380 pth.SplitPos = 0 381 } 382 } else { 383 pth.SplitPos = pth.NumList - m.PutPathL 384 } 385 // handle last hop signature 386 if m.LastSig == nil { 387 logger.Printf(logger.WARN, "[path] - last hop signature missing - path reset") 388 return path.NewPath(crypto.Hash(m.Block), m.Expire) 389 } 390 pth.LastSig = m.LastSig 391 pth.LastHop = sender 392 return pth 393 } 394 395 // Set path in message; corrects the message size accordingly 396 func (m *DHTP2PResultMsg) SetPath(p *path.Path) { 397 398 // return if recording is switched off (don't touch path) 399 if m.Flags&enums.DHT_RO_RECORD_ROUTE == 0 { 400 return 401 } 402 // compute old path size 403 var pes uint 404 if len(m.PathList) > 0 { 405 pes = m.PathList[0].Size() 406 } 407 oldSize := uint(len(m.PathList)) * pes 408 if m.TruncOrigin != nil { 409 oldSize += m.TruncOrigin.Size() 410 } 411 if m.LastSig != nil { 412 oldSize += m.LastSig.Size() 413 } 414 // if no new path is defined,... 415 if p == nil { 416 // ... remove existing path 417 m.TruncOrigin = nil 418 m.PathList = make([]*path.Entry, 0) 419 m.LastSig = nil 420 m.GetPathL = 0 421 m.PutPathL = 0 422 m.Flags &^= enums.DHT_RO_TRUNCATED 423 m.MsgSize -= uint16(oldSize) 424 return 425 } 426 // adjust message size 427 m.MsgSize += uint16(p.Size() - oldSize) 428 429 // transfer path data 430 if p.TruncOrigin != nil { 431 // truncated path 432 m.Flags |= enums.DHT_RO_TRUNCATED 433 m.TruncOrigin = p.TruncOrigin 434 } 435 m.PathList = util.Clone(p.List) 436 m.PutPathL = p.SplitPos 437 m.GetPathL = p.NumList - p.SplitPos 438 if p.LastSig != nil { 439 m.LastSig = p.LastSig 440 } 441 } 442 443 //---------------------------------------------------------------------- 444 445 // Update message (forwarding) 446 func (m *DHTP2PResultMsg) Update(pth *path.Path) *DHTP2PResultMsg { 447 // clone old message 448 msg := &DHTP2PResultMsg{ 449 MsgHeader: MsgHeader{m.MsgSize, m.MsgType}, 450 BType: m.BType, 451 Flags: m.Flags, 452 PutPathL: m.PutPathL, 453 GetPathL: m.GetPathL, 454 Expire: m.Expire, 455 Query: m.Query.Clone(), 456 TruncOrigin: m.TruncOrigin, 457 PathList: util.Clone(m.PathList), 458 LastSig: m.LastSig, 459 Block: util.Clone(m.Block), 460 } 461 // set new path 462 msg.SetPath(pth) 463 return msg 464 } 465 466 //---------------------------------------------------------------------- 467 468 // String returns a human-readable representation of the message. 469 func (m *DHTP2PResultMsg) String() string { 470 return fmt.Sprintf("DHTP2PResultMsg{btype=%s,putl=%d,getl=%d,flags=%s}", 471 m.BType, m.PutPathL, m.GetPathL, DHTFlags(m.Flags)) 472 } 473 474 //---------------------------------------------------------------------- 475 // DHT-P2P-HELLO 476 // 477 // A DHT-P2P-HELLO message is used to exchange information about transports 478 // with other DHT nodes. This struct is always followed by the actual 479 // network addresses of type "HelloAddress" 480 //---------------------------------------------------------------------- 481 482 // DHTP2PHelloMsg is a message send by peers to announce their presence 483 type DHTP2PHelloMsg struct { 484 MsgHeader 485 Reserved uint16 `order:"big"` // Reserved for further use 486 NumAddr uint16 `order:"big"` // Number of addresses in list 487 Signature *util.PeerSignature `init:"Init"` // Signature 488 Expire util.AbsoluteTime `` // expiration time 489 AddrList []byte `size:"*"` // List of end-point addresses (HelloAddress) 490 491 // transient state 492 addresses []*util.Address // list of converted addresses 493 } 494 495 // NewHelloMsgDHT creates an empty DHT_P2P_HELLO message. 496 func NewDHTP2PHelloMsg() *DHTP2PHelloMsg { 497 // return empty HelloMessage with set expire date 498 t := util.NewAbsoluteTime(time.Now().Add(HelloAddressExpiration)) 499 exp := util.NewAbsoluteTimeEpoch(t.Epoch()) 500 501 return &DHTP2PHelloMsg{ 502 MsgHeader: MsgHeader{80, enums.MSG_DHT_P2P_HELLO}, 503 Reserved: 0, // not used here 504 NumAddr: 0, // start with empty address list 505 Signature: util.NewPeerSignature(nil), // signature 506 Expire: exp, // default expiration 507 AddrList: make([]byte, 0), // list of addresses 508 } 509 } 510 511 // Init called after unmarshalling a message to setup internal state 512 func (m *DHTP2PHelloMsg) Init() (err error) { 513 if m.addresses != nil { 514 return 515 } 516 m.addresses = make([]*util.Address, 0) 517 var addr *util.Address 518 var as string 519 num, pos := 0, 0 520 for { 521 // parse address string from stream 522 if as, pos = util.ReadCString(m.AddrList, pos); pos == -1 { 523 break 524 } 525 if addr, err = util.ParseAddress(as); err != nil { 526 return 527 } 528 addr.Expire = m.Expire 529 m.addresses = append(m.addresses, addr) 530 num++ 531 } 532 // check numbers 533 if num != int(m.NumAddr) { 534 err = errors.New("number of addresses does not match") 535 } 536 return 537 } 538 539 // Addresses returns the list of HelloAddress 540 func (m *DHTP2PHelloMsg) Addresses() (list []*util.Address, err error) { 541 if m.addresses == nil { 542 err = errors.New("no addresses available") 543 return 544 } 545 return m.addresses, nil 546 } 547 548 // SetAddresses adds addresses to the HELLO message. 549 func (m *DHTP2PHelloMsg) SetAddresses(list []*util.Address) { 550 // write addresses as blob and track earliest expiration 551 t := util.NewAbsoluteTime(time.Now().Add(HelloAddressExpiration)) 552 exp := util.NewAbsoluteTimeEpoch(t.Epoch()) 553 wrt := new(bytes.Buffer) 554 for _, addr := range list { 555 // check if address expires before current expire 556 if exp.Compare(addr.Expire) > 0 { 557 exp = addr.Expire 558 } 559 n, _ := wrt.Write([]byte(addr.URI())) 560 wrt.WriteByte(0) 561 m.MsgSize += uint16(n + 1) 562 } 563 m.AddrList = wrt.Bytes() 564 m.Expire = exp 565 m.NumAddr = uint16(len(list)) 566 } 567 568 // String returns a human-readable representation of the message. 569 func (m *DHTP2PHelloMsg) String() string { 570 return fmt.Sprintf("DHTP2PHelloMsg{expire:%s,addrs=[%d]}", m.Expire, m.NumAddr) 571 } 572 573 // Verify the message signature 574 func (m *DHTP2PHelloMsg) Verify(peer *util.PeerID) (bool, error) { 575 // assemble signed data and public key 576 sd := m.SignedData() 577 pub := ed25519.NewPublicKeyFromBytes(peer.Data) 578 sig, err := ed25519.NewEdSignatureFromBytes(m.Signature.Data) 579 if err != nil { 580 return false, err 581 } 582 return pub.EdVerify(sd, sig) 583 } 584 585 // SetSignature stores a signature in the the HELLO block 586 func (m *DHTP2PHelloMsg) SetSignature(sig *util.PeerSignature) error { 587 m.Signature = sig 588 return nil 589 } 590 591 // SignedData assembles a data block for sign and verify operations. 592 func (m *DHTP2PHelloMsg) SignedData() []byte { 593 // hash address block 594 hAddr := sha512.Sum512(m.AddrList) 595 var size uint32 = 80 596 purpose := uint32(enums.SIG_HELLO) 597 598 // assemble signed data 599 buf := new(bytes.Buffer) 600 var n int 601 err := binary.Write(buf, binary.BigEndian, size) 602 if err == nil { 603 if err = binary.Write(buf, binary.BigEndian, purpose); err == nil { 604 if err = binary.Write(buf, binary.BigEndian, m.Expire); err == nil { 605 if n, err = buf.Write(hAddr[:]); err == nil { 606 if n != len(hAddr[:]) { 607 err = errors.New("write failed") 608 } 609 } 610 } 611 } 612 } 613 if err != nil { 614 logger.Printf(logger.ERROR, "[DHTP2PHelloMsg.SignedData] failed: %s", err.Error()) 615 } 616 return buf.Bytes() 617 } 618 619 //---------------------------------------------------------------------- 620 // Helper functions 621 //---------------------------------------------------------------------- 622 623 // get human-readable flags 624 func DHTFlags(flags uint16) string { 625 var list []string 626 if flags&enums.DHT_RO_DEMULTIPLEX_EVERYWHERE != 0 { 627 list = append(list, "DEMUX") 628 } 629 if flags&enums.DHT_RO_RECORD_ROUTE != 0 { 630 list = append(list, "ROUTE") 631 } 632 if flags&enums.DHT_RO_FIND_APPROXIMATE != 0 { 633 list = append(list, "APPROX") 634 } 635 if flags&enums.DHT_RO_TRUNCATED != 0 { 636 list = append(list, "TRUNC") 637 } 638 s := strings.Join(list, "|") 639 return "<" + s + ">" 640 }