1// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2// released under the MIT license
3
4package utils
5
6import (
7	"strings"
8	"time"
9)
10
11func IsRestrictedCTCPMessage(message string) bool {
12	// block all CTCP privmsgs to Tor clients except for ACTION
13	// DCC can potentially be used for deanonymization, the others for fingerprinting
14	return strings.HasPrefix(message, "\x01") && !strings.HasPrefix(message, "\x01ACTION")
15}
16
17type MessagePair struct {
18	Message string
19	Concat  bool // should be relayed with the multiline-concat tag
20}
21
22// SplitMessage represents a message that's been split for sending.
23// Two possibilities:
24// (a) Standard message that can be relayed on a single 512-byte line
25//     (MessagePair contains the message, Split == nil)
26// (b) multiline message that was split on the client side
27//     (Message == "", Split contains the split lines)
28type SplitMessage struct {
29	Message string
30	Msgid   string
31	Split   []MessagePair
32	Time    time.Time
33}
34
35func MakeMessage(original string) (result SplitMessage) {
36	result.Message = original
37	result.Msgid = GenerateSecretToken()
38	result.SetTime()
39
40	return
41}
42
43func (sm *SplitMessage) Append(message string, concat bool) {
44	if sm.Msgid == "" {
45		sm.Msgid = GenerateSecretToken()
46	}
47	sm.Split = append(sm.Split, MessagePair{
48		Message: message,
49		Concat:  concat,
50	})
51}
52
53func (sm *SplitMessage) SetTime() {
54	// strip the monotonic time, it's a potential source of problems:
55	sm.Time = time.Now().UTC().Round(0)
56}
57
58func (sm *SplitMessage) LenLines() int {
59	if sm.Split == nil {
60		if sm.Message == "" {
61			return 0
62		} else {
63			return 1
64		}
65	}
66	return len(sm.Split)
67}
68
69func (sm *SplitMessage) ValidMultiline() bool {
70	// must contain at least one nonblank line
71	for i := 0; i < len(sm.Split); i++ {
72		if len(sm.Split[i].Message) != 0 {
73			return true
74		}
75	}
76	return false
77}
78
79func (sm *SplitMessage) IsRestrictedCTCPMessage() bool {
80	if IsRestrictedCTCPMessage(sm.Message) {
81		return true
82	}
83	for i := 0; i < len(sm.Split); i++ {
84		if IsRestrictedCTCPMessage(sm.Split[i].Message) {
85			return true
86		}
87	}
88	return false
89}
90
91func (sm *SplitMessage) Is512() bool {
92	return sm.Split == nil
93}
94
95// TokenLineBuilder is a helper for building IRC lines composed of delimited tokens,
96// with a maximum line length.
97type TokenLineBuilder struct {
98	lineLen int
99	delim   string
100	buf     strings.Builder
101	result  []string
102}
103
104func (t *TokenLineBuilder) Initialize(lineLen int, delim string) {
105	t.lineLen = lineLen
106	t.delim = delim
107}
108
109// Add adds a token to the line, creating a new line if necessary.
110func (t *TokenLineBuilder) Add(token string) {
111	tokenLen := len(token)
112	if t.buf.Len() != 0 {
113		tokenLen += len(t.delim)
114	}
115	if t.lineLen < t.buf.Len()+tokenLen {
116		t.result = append(t.result, t.buf.String())
117		t.buf.Reset()
118	}
119	if t.buf.Len() != 0 {
120		t.buf.WriteString(t.delim)
121	}
122	t.buf.WriteString(token)
123}
124
125// Lines terminates the line-building and returns all the lines.
126func (t *TokenLineBuilder) Lines() (result []string) {
127	result = t.result
128	t.result = nil
129	if t.buf.Len() != 0 {
130		result = append(result, t.buf.String())
131		t.buf.Reset()
132	}
133	return
134}
135
136// BuildTokenLines is a convenience to apply TokenLineBuilder to a predetermined
137// slice of tokens.
138func BuildTokenLines(lineLen int, tokens []string, delim string) []string {
139	var tl TokenLineBuilder
140	tl.Initialize(lineLen, delim)
141	for _, arg := range tokens {
142		tl.Add(arg)
143	}
144	return tl.Lines()
145}
146