You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

message.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. // Copyright (c) 2016-2019 Daniel Oaks <daniel@danieloaks.net>
  2. // Copyright (c) 2018-2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
  3. // released under the ISC license
  4. package ircmsg
  5. import (
  6. "bytes"
  7. "errors"
  8. "strings"
  9. "unicode/utf8"
  10. )
  11. const (
  12. // "The size limit for message tags is 8191 bytes, including the leading
  13. // '@' (0x40) and trailing space ' ' (0x20) characters."
  14. MaxlenTags = 8191
  15. // MaxlenTags - ('@' + ' ')
  16. MaxlenTagData = MaxlenTags - 2
  17. // "Clients MUST NOT send messages with tag data exceeding 4094 bytes,
  18. // this includes tags with or without the client-only prefix."
  19. MaxlenClientTagData = 4094
  20. // "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
  21. MaxlenServerTagData = 4094
  22. // '@' + MaxlenClientTagData + ' '
  23. // this is the analogue of MaxlenTags when the source of the message is a client
  24. MaxlenTagsFromClient = MaxlenClientTagData + 2
  25. )
  26. var (
  27. // ErrorLineIsEmpty indicates that the given IRC line was empty.
  28. ErrorLineIsEmpty = errors.New("Line is empty")
  29. // ErrorLineContainsBadChar indicates that the line contained invalid characters
  30. ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
  31. // ErrorBodyTooLong indicates that the message body exceeded the specified
  32. // length limit (typically 512 bytes). This error is non-fatal; if encountered
  33. // when parsing a message, the message is parsed up to the length limit, and
  34. // if encountered when serializing a message, the message is truncated to the limit.
  35. ErrorBodyTooLong = errors.New("Line body exceeded the specified length limit; outgoing messages will be truncated")
  36. // ErrorTagsTooLong indicates that the message exceeded the maximum tag length
  37. // (the specified response on the server side is 417 ERR_INPUTTOOLONG).
  38. ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")
  39. // ErrorInvalidTagContent indicates that a tag name or value was invalid
  40. ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")
  41. // ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
  42. ErrorCommandMissing = errors.New("IRC messages MUST have a command")
  43. // ErrorBadParam indicates that an IRC message could not be serialized because
  44. // its parameters violated the syntactic constraints on IRC parameters:
  45. // non-final parameters cannot be empty, contain a space, or start with `:`.
  46. ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
  47. )
  48. // Message represents an IRC message, as defined by the RFCs and as
  49. // extended by the IRCv3 Message Tags specification with the introduction
  50. // of message tags.
  51. type Message struct {
  52. Source string
  53. Command string
  54. Params []string
  55. forceTrailing bool
  56. tags map[string]string
  57. clientOnlyTags map[string]string
  58. }
  59. // ForceTrailing ensures that when the message is serialized, the final parameter
  60. // will be encoded as a "trailing parameter" (preceded by a colon). This is
  61. // almost never necessary and should not be used except when having to interact
  62. // with broken implementations that don't correctly interpret IRC messages.
  63. func (msg *Message) ForceTrailing() {
  64. msg.forceTrailing = true
  65. }
  66. // GetTag returns whether a tag is present, and if so, what its value is.
  67. func (msg *Message) GetTag(tagName string) (present bool, value string) {
  68. if len(tagName) == 0 {
  69. return
  70. } else if tagName[0] == '+' {
  71. value, present = msg.clientOnlyTags[tagName]
  72. return
  73. } else {
  74. value, present = msg.tags[tagName]
  75. return
  76. }
  77. }
  78. // HasTag returns whether a tag is present.
  79. func (msg *Message) HasTag(tagName string) (present bool) {
  80. present, _ = msg.GetTag(tagName)
  81. return
  82. }
  83. // SetTag sets a tag.
  84. func (msg *Message) SetTag(tagName, tagValue string) {
  85. if len(tagName) == 0 {
  86. return
  87. } else if tagName[0] == '+' {
  88. if msg.clientOnlyTags == nil {
  89. msg.clientOnlyTags = make(map[string]string)
  90. }
  91. msg.clientOnlyTags[tagName] = tagValue
  92. } else {
  93. if msg.tags == nil {
  94. msg.tags = make(map[string]string)
  95. }
  96. msg.tags[tagName] = tagValue
  97. }
  98. }
  99. // DeleteTag deletes a tag.
  100. func (msg *Message) DeleteTag(tagName string) {
  101. if len(tagName) == 0 {
  102. return
  103. } else if tagName[0] == '+' {
  104. delete(msg.clientOnlyTags, tagName)
  105. } else {
  106. delete(msg.tags, tagName)
  107. }
  108. }
  109. // UpdateTags is a convenience to set multiple tags at once.
  110. func (msg *Message) UpdateTags(tags map[string]string) {
  111. for name, value := range tags {
  112. msg.SetTag(name, value)
  113. }
  114. }
  115. // AllTags returns all tags as a single map.
  116. func (msg *Message) AllTags() (result map[string]string) {
  117. result = make(map[string]string, len(msg.tags)+len(msg.clientOnlyTags))
  118. for name, value := range msg.tags {
  119. result[name] = value
  120. }
  121. for name, value := range msg.clientOnlyTags {
  122. result[name] = value
  123. }
  124. return
  125. }
  126. // ClientOnlyTags returns the client-only tags (the tags with the + prefix).
  127. // The returned map may be internal storage of the Message object and
  128. // should not be modified.
  129. func (msg *Message) ClientOnlyTags() map[string]string {
  130. return msg.clientOnlyTags
  131. }
  132. // Nick returns the name component of the message source (typically a nickname,
  133. // but possibly a server name).
  134. func (msg *Message) Nick() (nick string) {
  135. nuh, err := ParseNUH(msg.Source)
  136. if err == nil {
  137. return nuh.Name
  138. }
  139. return
  140. }
  141. // NUH returns the source of the message as a parsed NUH ("nick-user-host");
  142. // if the source is not well-formed as a NUH, it returns an error.
  143. func (msg *Message) NUH() (nuh NUH, err error) {
  144. return ParseNUH(msg.Source)
  145. }
  146. // ParseLine creates and returns a message from the given IRC line.
  147. func ParseLine(line string) (ircmsg Message, err error) {
  148. return parseLine(line, 0, 0)
  149. }
  150. // ParseLineStrict creates and returns an Message from the given IRC line,
  151. // taking the maximum length into account and truncating the message as appropriate.
  152. // If fromClient is true, it enforces the client limit on tag data length (4094 bytes),
  153. // allowing the server to return ERR_INPUTTOOLONG as appropriate. If truncateLen is
  154. // nonzero, it is the length at which the non-tag portion of the message is truncated.
  155. func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg Message, err error) {
  156. maxTagDataLength := MaxlenTagData
  157. if fromClient {
  158. maxTagDataLength = MaxlenClientTagData
  159. }
  160. return parseLine(line, maxTagDataLength, truncateLen)
  161. }
  162. // slice off any amount of ' ' from the front of the string
  163. func trimInitialSpaces(str string) string {
  164. var i int
  165. for i = 0; i < len(str) && str[i] == ' '; i++ {
  166. }
  167. return str[i:]
  168. }
  169. func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
  170. // remove either \n or \r\n from the end of the line:
  171. line = strings.TrimSuffix(line, "\n")
  172. line = strings.TrimSuffix(line, "\r")
  173. // whether we removed them ourselves, or whether they were removed previously,
  174. // they count against the line limit:
  175. if truncateLen != 0 {
  176. if truncateLen <= 2 {
  177. return ircmsg, ErrorLineIsEmpty
  178. }
  179. truncateLen -= 2
  180. }
  181. // now validate for the 3 forbidden bytes:
  182. if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
  183. return ircmsg, ErrorLineContainsBadChar
  184. }
  185. if len(line) < 1 {
  186. return ircmsg, ErrorLineIsEmpty
  187. }
  188. // tags
  189. if line[0] == '@' {
  190. tagEnd := strings.IndexByte(line, ' ')
  191. if tagEnd == -1 {
  192. return ircmsg, ErrorLineIsEmpty
  193. }
  194. tags := line[1:tagEnd]
  195. if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
  196. return ircmsg, ErrorTagsTooLong
  197. }
  198. err = ircmsg.parseTags(tags)
  199. if err != nil {
  200. return
  201. }
  202. // skip over the tags and the separating space
  203. line = line[tagEnd+1:]
  204. }
  205. // truncate if desired
  206. if truncateLen != 0 && truncateLen < len(line) {
  207. err = ErrorBodyTooLong
  208. line = TruncateUTF8Safe(line, truncateLen)
  209. }
  210. // modern: "These message parts, and parameters themselves, are separated
  211. // by one or more ASCII SPACE characters"
  212. line = trimInitialSpaces(line)
  213. // source
  214. if 0 < len(line) && line[0] == ':' {
  215. sourceEnd := strings.IndexByte(line, ' ')
  216. if sourceEnd == -1 {
  217. return ircmsg, ErrorLineIsEmpty
  218. }
  219. ircmsg.Source = line[1:sourceEnd]
  220. // skip over the source and the separating space
  221. line = line[sourceEnd+1:]
  222. }
  223. line = trimInitialSpaces(line)
  224. // command
  225. commandEnd := strings.IndexByte(line, ' ')
  226. paramStart := commandEnd + 1
  227. if commandEnd == -1 {
  228. commandEnd = len(line)
  229. paramStart = len(line)
  230. }
  231. // normalize command to uppercase:
  232. ircmsg.Command = strings.ToUpper(line[:commandEnd])
  233. if len(ircmsg.Command) == 0 {
  234. return ircmsg, ErrorLineIsEmpty
  235. }
  236. line = line[paramStart:]
  237. for {
  238. line = trimInitialSpaces(line)
  239. if len(line) == 0 {
  240. break
  241. }
  242. // handle trailing
  243. if line[0] == ':' {
  244. ircmsg.Params = append(ircmsg.Params, line[1:])
  245. break
  246. }
  247. paramEnd := strings.IndexByte(line, ' ')
  248. if paramEnd == -1 {
  249. ircmsg.Params = append(ircmsg.Params, line)
  250. break
  251. }
  252. ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
  253. line = line[paramEnd+1:]
  254. }
  255. return ircmsg, err
  256. }
  257. // helper to parse tags
  258. func (ircmsg *Message) parseTags(tags string) (err error) {
  259. for 0 < len(tags) {
  260. tagEnd := strings.IndexByte(tags, ';')
  261. endPos := tagEnd
  262. nextPos := tagEnd + 1
  263. if tagEnd == -1 {
  264. endPos = len(tags)
  265. nextPos = len(tags)
  266. }
  267. tagPair := tags[:endPos]
  268. equalsIndex := strings.IndexByte(tagPair, '=')
  269. var tagName, tagValue string
  270. if equalsIndex == -1 {
  271. // tag with no value
  272. tagName = tagPair
  273. } else {
  274. tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
  275. }
  276. // "Implementations [...] MUST NOT perform any validation that would
  277. // reject the message if an invalid tag key name is used."
  278. if validateTagName(tagName) {
  279. if !validateTagValue(tagValue) {
  280. return ErrorInvalidTagContent
  281. }
  282. ircmsg.SetTag(tagName, UnescapeTagValue(tagValue))
  283. }
  284. // skip over the tag just processed, plus the delimiting ; if any
  285. tags = tags[nextPos:]
  286. }
  287. return nil
  288. }
  289. // MakeMessage provides a simple way to create a new Message.
  290. func MakeMessage(tags map[string]string, source string, command string, params ...string) (ircmsg Message) {
  291. ircmsg.Source = source
  292. ircmsg.Command = command
  293. ircmsg.Params = params
  294. ircmsg.UpdateTags(tags)
  295. return ircmsg
  296. }
  297. // Line returns a sendable line created from an Message.
  298. func (ircmsg *Message) Line() (result string, err error) {
  299. bytes, err := ircmsg.line(0, 0, 0, 0)
  300. if err == nil {
  301. result = string(bytes)
  302. }
  303. return
  304. }
  305. // LineBytes returns a sendable line created from an Message.
  306. func (ircmsg *Message) LineBytes() (result []byte, err error) {
  307. result, err = ircmsg.line(0, 0, 0, 0)
  308. return
  309. }
  310. // LineBytesStrict returns a sendable line, as a []byte, created from an Message.
  311. // fromClient controls whether the server-side or client-side tag length limit
  312. // is enforced. If truncateLen is nonzero, it is the length at which the
  313. // non-tag portion of the message is truncated.
  314. func (ircmsg *Message) LineBytesStrict(fromClient bool, truncateLen int) ([]byte, error) {
  315. var tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit int
  316. if fromClient {
  317. // enforce client max tags:
  318. // <client_max> (4096) :: '@' <tag_data 4094> ' '
  319. tagLimit = MaxlenTagsFromClient
  320. } else {
  321. // on the server side, enforce separate client-only and server-added tag budgets:
  322. // "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
  323. // <combined_max> (8191) :: '@' <tag_data 4094> ';' <tag_data 4094> ' '
  324. clientOnlyTagDataLimit = MaxlenClientTagData
  325. serverAddedTagDataLimit = MaxlenServerTagData
  326. }
  327. return ircmsg.line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen)
  328. }
  329. func paramRequiresTrailing(param string) bool {
  330. return len(param) == 0 || strings.IndexByte(param, ' ') != -1 || param[0] == ':'
  331. }
  332. // line returns a sendable line created from an Message.
  333. func (ircmsg *Message) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
  334. if len(ircmsg.Command) == 0 {
  335. return nil, ErrorCommandMissing
  336. }
  337. var buf bytes.Buffer
  338. // write the tags, computing the budgets for client-only tags and regular tags
  339. var lenRegularTags, lenClientOnlyTags, lenTags int
  340. if 0 < len(ircmsg.tags) || 0 < len(ircmsg.clientOnlyTags) {
  341. var tagError error
  342. buf.WriteByte('@')
  343. firstTag := true
  344. writeTags := func(tags map[string]string) {
  345. for tag, val := range tags {
  346. if !(validateTagName(tag) && validateTagValue(val)) {
  347. tagError = ErrorInvalidTagContent
  348. }
  349. if !firstTag {
  350. buf.WriteByte(';') // delimiter
  351. }
  352. buf.WriteString(tag)
  353. if val != "" {
  354. buf.WriteByte('=')
  355. buf.WriteString(EscapeTagValue(val))
  356. }
  357. firstTag = false
  358. }
  359. }
  360. writeTags(ircmsg.tags)
  361. lenRegularTags = buf.Len() - 1 // '@' is not counted
  362. writeTags(ircmsg.clientOnlyTags)
  363. lenClientOnlyTags = (buf.Len() - 1) - lenRegularTags // '@' is not counted
  364. if lenRegularTags != 0 {
  365. // semicolon between regular and client-only tags is not counted
  366. lenClientOnlyTags -= 1
  367. }
  368. buf.WriteByte(' ')
  369. if tagError != nil {
  370. return nil, tagError
  371. }
  372. }
  373. lenTags = buf.Len()
  374. if 0 < tagLimit && tagLimit < buf.Len() {
  375. return nil, ErrorTagsTooLong
  376. }
  377. if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
  378. return nil, ErrorTagsTooLong
  379. }
  380. if len(ircmsg.Source) > 0 {
  381. buf.WriteByte(':')
  382. buf.WriteString(ircmsg.Source)
  383. buf.WriteByte(' ')
  384. }
  385. buf.WriteString(ircmsg.Command)
  386. for i, param := range ircmsg.Params {
  387. buf.WriteByte(' ')
  388. requiresTrailing := paramRequiresTrailing(param)
  389. lastParam := i == len(ircmsg.Params)-1
  390. if (requiresTrailing || ircmsg.forceTrailing) && lastParam {
  391. buf.WriteByte(':')
  392. } else if requiresTrailing && !lastParam {
  393. return nil, ErrorBadParam
  394. }
  395. buf.WriteString(param)
  396. }
  397. // truncate if desired; leave 2 bytes over for \r\n:
  398. if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
  399. err = ErrorBodyTooLong
  400. newBufLen := lenTags + (truncateLen - 2)
  401. buf.Truncate(newBufLen)
  402. // XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
  403. // if so, remove additional bytes, stopping when the sequence either
  404. // ends in a valid codepoint, or we have removed 3 bytes (the maximum
  405. // length of the remnant of a once-valid, truncated codepoint; we don't
  406. // want to truncate the entire message if it wasn't UTF8 in the first
  407. // place).
  408. for i := 0; i < (utf8.UTFMax - 1); i++ {
  409. r, n := utf8.DecodeLastRune(buf.Bytes())
  410. if r == utf8.RuneError && n <= 1 {
  411. newBufLen--
  412. buf.Truncate(newBufLen)
  413. } else {
  414. break
  415. }
  416. }
  417. }
  418. buf.WriteString("\r\n")
  419. result = buf.Bytes()
  420. toValidate := result[:len(result)-2]
  421. if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
  422. return nil, ErrorLineContainsBadChar
  423. }
  424. return result, err
  425. }