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

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