// Copyright (c) 2017 Daniel Oaks // released under the MIT license package utils import ( "crypto/sha256" "encoding/json" "fmt" "github.com/cshum/imagor/imagorpath" "github.com/ergochat/irc-go/ircmsg" "net/http" "os" "regexp" "strings" "time" ) func IsRestrictedCTCPMessage(message string) bool { // block all CTCP privmsgs to Tor clients except for ACTION // DCC can potentially be used for deanonymization, the others for fingerprinting return strings.HasPrefix(message, "\x01") && !strings.HasPrefix(message, "\x01ACTION") } type MessagePair struct { Message string Tags map[string]string Concat bool // should be relayed with the multiline-concat tag } // SplitMessage represents a message that's been split for sending. // Two possibilities: // (a) Standard message that can be relayed on a single 512-byte line // // (MessagePair contains the message, Split == nil) // // (b) multiline message that was split on the client side // // (Message == "", Split contains the split lines) type SplitMessage struct { Message string Msgid string Split []MessagePair Time time.Time } func MakeMessage(original string) (result SplitMessage) { result.Message = original result.Msgid = GenerateMessageIdStr() result.SetTime() return } func (sm *SplitMessage) Append(message string, concat bool, tags map[string]string) { if sm.Msgid == "" { sm.Msgid = GenerateMessageIdStr() } sm.Split = append(sm.Split, MessagePair{ Message: message, Concat: concat, Tags: tags, }) } func (sm *SplitMessage) SetTime() { // strip the monotonic time, it's a potential source of problems: sm.Time = time.Now().UTC().Round(0) } func (sm *SplitMessage) LenLines() int { if sm.Split == nil { if sm.Message == "" { return 0 } else { return 1 } } return len(sm.Split) } func (sm *SplitMessage) ValidMultiline() bool { // must contain at least one nonblank line for i := 0; i < len(sm.Split); i++ { if len(sm.Split[i].Message) != 0 { return true } } return false } func (sm *SplitMessage) IsRestrictedCTCPMessage() bool { if IsRestrictedCTCPMessage(sm.Message) { return true } for i := 0; i < len(sm.Split); i++ { if IsRestrictedCTCPMessage(sm.Split[i].Message) { return true } } return false } func (sm *SplitMessage) Is512() bool { return sm.Split == nil } // TokenLineBuilder is a helper for building IRC lines composed of delimited tokens, // with a maximum line length. type TokenLineBuilder struct { lineLen int delim string buf strings.Builder result []string } func (t *TokenLineBuilder) Initialize(lineLen int, delim string) { t.lineLen = lineLen t.delim = delim } // Add adds a token to the line, creating a new line if necessary. func (t *TokenLineBuilder) Add(token string) { tokenLen := len(token) if t.buf.Len() != 0 { tokenLen += len(t.delim) } if t.lineLen < t.buf.Len()+tokenLen { t.result = append(t.result, t.buf.String()) t.buf.Reset() } if t.buf.Len() != 0 { t.buf.WriteString(t.delim) } t.buf.WriteString(token) } // AddParts concatenates `parts` into a token and adds it to the line, // creating a new line if necessary. func (t *TokenLineBuilder) AddParts(parts ...string) { var tokenLen int for _, part := range parts { tokenLen += len(part) } if t.buf.Len() != 0 { tokenLen += len(t.delim) } if t.lineLen < t.buf.Len()+tokenLen { t.result = append(t.result, t.buf.String()) t.buf.Reset() } if t.buf.Len() != 0 { t.buf.WriteString(t.delim) } for _, part := range parts { t.buf.WriteString(part) } } // Lines terminates the line-building and returns all the lines. func (t *TokenLineBuilder) Lines() (result []string) { result = t.result t.result = nil if t.buf.Len() != 0 { result = append(result, t.buf.String()) t.buf.Reset() } return } // BuildTokenLines is a convenience to apply TokenLineBuilder to a predetermined // slice of tokens. func BuildTokenLines(lineLen int, tokens []string, delim string) []string { var tl TokenLineBuilder tl.Initialize(lineLen, delim) for _, arg := range tokens { tl.Add(arg) } return tl.Lines() } var urlRegex = regexp.MustCompile("https?:\\/\\/[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?") func GenerateImagorSignaturesFromMessage(message *ircmsg.Message) string { line, err := message.Line() if err == nil { return GenerateImagorSignatures(line) } return "" } var secretKey = os.Getenv("IMAGOR_SECRET") var baseUrl = os.Getenv("IMAGOR_URL") func GetUrlMime(url string) string { // hacky, should fix if !strings.Contains(url, "?") { url += "?" } params := imagorpath.Params{ Image: url, Meta: true, } metaPath := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, secretKey)) client := http.Client{ Timeout: 5 * time.Second, } resp, err := client.Get(baseUrl + metaPath) if err != nil { println("Failed on the initial get") println(err.Error()) return "" } defer resp.Body.Close() var meta map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&meta) if err != nil { println("Failed on the JSON decode") return "" } contentType, valid := meta["format"].(string) fmt.Printf("%+v\n", meta) if !valid { println("No content type") return "" } return contentType } // Process a message to add Imagor signatures func GenerateImagorSignatures(str string) string { urls := urlRegex.FindAllString(str, -1) var sigs []string for _, url := range urls { params := imagorpath.Params{ Image: url, FitIn: true, Width: 600, Height: 600, } path := imagorpath.Generate(params, imagorpath.NewHMACSigner(sha256.New, 0, secretKey)) signature := path[:strings.IndexByte(path, '/')] contentType := GetUrlMime(url) if contentType != "" { sigs = append(sigs, signature+"|"+strings.ReplaceAll(contentType, "/", "_")) } else { sigs = append(sigs, signature) } } if len(sigs) > 0 { return strings.Join(sigs, ",") } return "" }