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

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