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 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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. Prefix 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. // ParseLine creates and returns a message from the given IRC line.
  133. func ParseLine(line string) (ircmsg Message, err error) {
  134. return parseLine(line, 0, 0)
  135. }
  136. // ParseLineStrict creates and returns an Message from the given IRC line,
  137. // taking the maximum length into account and truncating the message as appropriate.
  138. // If fromClient is true, it enforces the client limit on tag data length (4094 bytes),
  139. // allowing the server to return ERR_INPUTTOOLONG as appropriate. If truncateLen is
  140. // nonzero, it is the length at which the non-tag portion of the message is truncated.
  141. func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg Message, err error) {
  142. maxTagDataLength := MaxlenTagData
  143. if fromClient {
  144. maxTagDataLength = MaxlenClientTagData
  145. }
  146. return parseLine(line, maxTagDataLength, truncateLen)
  147. }
  148. // slice off any amount of ' ' from the front of the string
  149. func trimInitialSpaces(str string) string {
  150. var i int
  151. for i = 0; i < len(str) && str[i] == ' '; i++ {
  152. }
  153. return str[i:]
  154. }
  155. func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg Message, err error) {
  156. // remove either \n or \r\n from the end of the line:
  157. line = strings.TrimSuffix(line, "\n")
  158. line = strings.TrimSuffix(line, "\r")
  159. // whether we removed them ourselves, or whether they were removed previously,
  160. // they count against the line limit:
  161. if truncateLen != 0 {
  162. if truncateLen <= 2 {
  163. return ircmsg, ErrorLineIsEmpty
  164. }
  165. truncateLen -= 2
  166. }
  167. // now validate for the 3 forbidden bytes:
  168. if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
  169. return ircmsg, ErrorLineContainsBadChar
  170. }
  171. if len(line) < 1 {
  172. return ircmsg, ErrorLineIsEmpty
  173. }
  174. // tags
  175. if line[0] == '@' {
  176. tagEnd := strings.IndexByte(line, ' ')
  177. if tagEnd == -1 {
  178. return ircmsg, ErrorLineIsEmpty
  179. }
  180. tags := line[1:tagEnd]
  181. if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
  182. return ircmsg, ErrorTagsTooLong
  183. }
  184. err = ircmsg.parseTags(tags)
  185. if err != nil {
  186. return
  187. }
  188. // skip over the tags and the separating space
  189. line = line[tagEnd+1:]
  190. }
  191. // truncate if desired
  192. if truncateLen != 0 && truncateLen < len(line) {
  193. err = ErrorBodyTooLong
  194. line = line[:truncateLen]
  195. }
  196. // modern: "These message parts, and parameters themselves, are separated
  197. // by one or more ASCII SPACE characters"
  198. line = trimInitialSpaces(line)
  199. // prefix
  200. if 0 < len(line) && line[0] == ':' {
  201. prefixEnd := strings.IndexByte(line, ' ')
  202. if prefixEnd == -1 {
  203. return ircmsg, ErrorLineIsEmpty
  204. }
  205. ircmsg.Prefix = line[1:prefixEnd]
  206. // skip over the prefix and the separating space
  207. line = line[prefixEnd+1:]
  208. }
  209. line = trimInitialSpaces(line)
  210. // command
  211. commandEnd := strings.IndexByte(line, ' ')
  212. paramStart := commandEnd + 1
  213. if commandEnd == -1 {
  214. commandEnd = len(line)
  215. paramStart = len(line)
  216. }
  217. // normalize command to uppercase:
  218. ircmsg.Command = strings.ToUpper(line[:commandEnd])
  219. if len(ircmsg.Command) == 0 {
  220. return ircmsg, ErrorLineIsEmpty
  221. }
  222. line = line[paramStart:]
  223. for {
  224. line = trimInitialSpaces(line)
  225. if len(line) == 0 {
  226. break
  227. }
  228. // handle trailing
  229. if line[0] == ':' {
  230. ircmsg.Params = append(ircmsg.Params, line[1:])
  231. break
  232. }
  233. paramEnd := strings.IndexByte(line, ' ')
  234. if paramEnd == -1 {
  235. ircmsg.Params = append(ircmsg.Params, line)
  236. break
  237. }
  238. ircmsg.Params = append(ircmsg.Params, line[:paramEnd])
  239. line = line[paramEnd+1:]
  240. }
  241. return ircmsg, err
  242. }
  243. // helper to parse tags
  244. func (ircmsg *Message) parseTags(tags string) (err error) {
  245. for 0 < len(tags) {
  246. tagEnd := strings.IndexByte(tags, ';')
  247. endPos := tagEnd
  248. nextPos := tagEnd + 1
  249. if tagEnd == -1 {
  250. endPos = len(tags)
  251. nextPos = len(tags)
  252. }
  253. tagPair := tags[:endPos]
  254. equalsIndex := strings.IndexByte(tagPair, '=')
  255. var tagName, tagValue string
  256. if equalsIndex == -1 {
  257. // tag with no value
  258. tagName = tagPair
  259. } else {
  260. tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
  261. }
  262. // "Implementations [...] MUST NOT perform any validation that would
  263. // reject the message if an invalid tag key name is used."
  264. if validateTagName(tagName) {
  265. if !validateTagValue(tagValue) {
  266. return ErrorInvalidTagContent
  267. }
  268. ircmsg.SetTag(tagName, UnescapeTagValue(tagValue))
  269. }
  270. // skip over the tag just processed, plus the delimiting ; if any
  271. tags = tags[nextPos:]
  272. }
  273. return nil
  274. }
  275. // MakeMessage provides a simple way to create a new Message.
  276. func MakeMessage(tags map[string]string, prefix string, command string, params ...string) (ircmsg Message) {
  277. ircmsg.Prefix = prefix
  278. ircmsg.Command = command
  279. ircmsg.Params = params
  280. ircmsg.UpdateTags(tags)
  281. return ircmsg
  282. }
  283. // Line returns a sendable line created from an Message.
  284. func (ircmsg *Message) Line() (result string, err error) {
  285. bytes, err := ircmsg.line(0, 0, 0, 0)
  286. if err == nil {
  287. result = string(bytes)
  288. }
  289. return
  290. }
  291. // LineBytes returns a sendable line created from an Message.
  292. func (ircmsg *Message) LineBytes() (result []byte, err error) {
  293. result, err = ircmsg.line(0, 0, 0, 0)
  294. return
  295. }
  296. // LineBytesStrict returns a sendable line, as a []byte, created from an Message.
  297. // fromClient controls whether the server-side or client-side tag length limit
  298. // is enforced. If truncateLen is nonzero, it is the length at which the
  299. // non-tag portion of the message is truncated.
  300. func (ircmsg *Message) LineBytesStrict(fromClient bool, truncateLen int) ([]byte, error) {
  301. var tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit int
  302. if fromClient {
  303. // enforce client max tags:
  304. // <client_max> (4096) :: '@' <tag_data 4094> ' '
  305. tagLimit = MaxlenTagsFromClient
  306. } else {
  307. // on the server side, enforce separate client-only and server-added tag budgets:
  308. // "Servers MUST NOT add tag data exceeding 4094 bytes to messages."
  309. // <combined_max> (8191) :: '@' <tag_data 4094> ';' <tag_data 4094> ' '
  310. clientOnlyTagDataLimit = MaxlenClientTagData
  311. serverAddedTagDataLimit = MaxlenServerTagData
  312. }
  313. return ircmsg.line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen)
  314. }
  315. func paramRequiresTrailing(param string) bool {
  316. return len(param) == 0 || strings.IndexByte(param, ' ') != -1 || param[0] == ':'
  317. }
  318. // line returns a sendable line created from an Message.
  319. func (ircmsg *Message) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
  320. if len(ircmsg.Command) == 0 {
  321. return nil, ErrorCommandMissing
  322. }
  323. var buf bytes.Buffer
  324. // write the tags, computing the budgets for client-only tags and regular tags
  325. var lenRegularTags, lenClientOnlyTags, lenTags int
  326. if 0 < len(ircmsg.tags) || 0 < len(ircmsg.clientOnlyTags) {
  327. var tagError error
  328. buf.WriteByte('@')
  329. firstTag := true
  330. writeTags := func(tags map[string]string) {
  331. for tag, val := range tags {
  332. if !(validateTagName(tag) && validateTagValue(val)) {
  333. tagError = ErrorInvalidTagContent
  334. }
  335. if !firstTag {
  336. buf.WriteByte(';') // delimiter
  337. }
  338. buf.WriteString(tag)
  339. if val != "" {
  340. buf.WriteByte('=')
  341. buf.WriteString(EscapeTagValue(val))
  342. }
  343. firstTag = false
  344. }
  345. }
  346. writeTags(ircmsg.tags)
  347. lenRegularTags = buf.Len() - 1 // '@' is not counted
  348. writeTags(ircmsg.clientOnlyTags)
  349. lenClientOnlyTags = (buf.Len() - 1) - lenRegularTags // '@' is not counted
  350. if lenRegularTags != 0 {
  351. // semicolon between regular and client-only tags is not counted
  352. lenClientOnlyTags -= 1
  353. }
  354. buf.WriteByte(' ')
  355. if tagError != nil {
  356. return nil, tagError
  357. }
  358. }
  359. lenTags = buf.Len()
  360. if 0 < tagLimit && tagLimit < buf.Len() {
  361. return nil, ErrorTagsTooLong
  362. }
  363. if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
  364. return nil, ErrorTagsTooLong
  365. }
  366. if len(ircmsg.Prefix) > 0 {
  367. buf.WriteByte(':')
  368. buf.WriteString(ircmsg.Prefix)
  369. buf.WriteByte(' ')
  370. }
  371. buf.WriteString(ircmsg.Command)
  372. for i, param := range ircmsg.Params {
  373. buf.WriteByte(' ')
  374. requiresTrailing := paramRequiresTrailing(param)
  375. lastParam := i == len(ircmsg.Params)-1
  376. if (requiresTrailing || ircmsg.forceTrailing) && lastParam {
  377. buf.WriteByte(':')
  378. } else if requiresTrailing && !lastParam {
  379. return nil, ErrorBadParam
  380. }
  381. buf.WriteString(param)
  382. }
  383. // truncate if desired; leave 2 bytes over for \r\n:
  384. if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
  385. err = ErrorBodyTooLong
  386. newBufLen := lenTags + (truncateLen - 2)
  387. buf.Truncate(newBufLen)
  388. // XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
  389. // if so, remove additional bytes, stopping when the sequence either
  390. // ends in a valid codepoint, or we have removed 3 bytes (the maximum
  391. // length of the remnant of a once-valid, truncated codepoint; we don't
  392. // want to truncate the entire message if it wasn't UTF8 in the first
  393. // place).
  394. for i := 0; i < (utf8.UTFMax - 1); i++ {
  395. r, n := utf8.DecodeLastRune(buf.Bytes())
  396. if r == utf8.RuneError && n <= 1 {
  397. newBufLen--
  398. buf.Truncate(newBufLen)
  399. } else {
  400. break
  401. }
  402. }
  403. }
  404. buf.WriteString("\r\n")
  405. result = buf.Bytes()
  406. toValidate := result[:len(result)-2]
  407. if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
  408. return nil, ErrorLineContainsBadChar
  409. }
  410. return result, err
  411. }