...
 
Commits (3)
......@@ -9,3 +9,4 @@ Coverage report is available at [![coverage report](https://git.bella.network/th
- Don't depend on MAC address API
- Download fom http://standards-oui.ieee.org/oui.txt, parse and update periodically
- Migrate all commands from https://github.com/Thomas2500/uTeleBot/tree/master/commands
package main
var availableCommands = map[string]string{
"dns": "Perform a DNS lookup",
"mac": "Get the vendor of a MAC address",
"dns": "Perform a DNS lookup",
"mac": "Get the vendor of a MAC address",
"ping": "Perform a ping on a IPv4/IPv6 or domain",
"stats": "We all love statistics",
"traceroute": "Perform a traceroute",
"whoami": "Who am I?",
}
func commandHelp() string {
......
......@@ -39,7 +39,12 @@ func main() {
// Start http server and listen for incoming data
updates := bot.ListenForWebhook("/")
go http.ListenAndServe("0.0.0.0:5505", nil)
go func() {
err := http.ListenAndServe("0.0.0.0:5505", nil)
if err != nil {
log.Fatal(err)
}
}()
// Process incoming messages
for update := range updates {
......
package main
import (
"strconv"
"strings"
"git.bella.network/playground/golang/internal/dnsfetch"
"git.bella.network/playground/golang/internal/macfetch"
"git.bella.network/playground/golang/internal/pingcomm"
"git.bella.network/playground/golang/internal/stats"
"git.bella.network/playground/golang/internal/traceroutecomm"
"git.bella.network/playground/golang/internal/dnsfetch"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)
func processMessage(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
// TODO: Track user statictics
// check if message was adressed to bot
if update.Message.IsCommand() {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
......@@ -47,10 +49,45 @@ func processMessage(bot *tgbotapi.BotAPI, update tgbotapi.Update) {
} else {
msg.Text = macfetch.GetStringResponse(args[0])
}
case "ping":
if len(args) == 0 || len(args[0]) == 0 {
msg.Text = pingcomm.GetHelp()
} else {
msg.Text = pingcomm.GetStringResponse(args[0])
}
case "traceroute":
if len(args) == 0 || len(args[0]) == 0 {
msg.Text = traceroutecomm.GetHelp()
} else {
go bot.Send(tgbotapi.NewMessage(update.Message.Chat.ID, "Performing traceroute. This will take a few moments ..."))
msg.Text = traceroutecomm.GetStringResponse(args[0])
msg.ReplyToMessageID = update.Message.MessageID
}
case "whoami":
name := update.Message.From.UserName
if name == "" {
name = update.Message.From.FirstName
}
msg.Text = "*Hello " + name + ".*\nYou have the user ID " + strconv.Itoa(update.Message.From.ID) + ".\n"
msg.Text += "This chat type is *" + update.Message.Chat.Type + "*."
case "stats":
stats := stats.GetStringResponse(update.Message.Chat.ID)
for _, stat := range stats {
msg.Text = stat
bot.Send(msg)
}
return
default:
// We don't know this command - skip it
return
}
bot.Send(msg)
} else {
stats.AddRecord(update)
}
}
module git.bella.network/playground/golang
require (
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
)
......@@ -91,7 +91,7 @@ func GetStringResponse(target string, record string) string {
return responseText
}
// GetHelp returns
// GetHelp returns the help text of the command
func GetHelp() string {
help := "*DNS Query Command*\nThe command `/dns example.com` requests a basic `A` lookup to the given domain. The command can be extended with a record type like `/dns example.com AAAA`\n\n*Allowed records:*\n"
i := 0
......
package pingcomm
import (
"net"
"os/exec"
govalidator "github.com/asaskevich/govalidator"
)
var privateIPBlocks []*net.IPNet
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fd00::/8", // IPv6 private address space
} {
_, block, _ := net.ParseCIDR(cidr)
privateIPBlocks = append(privateIPBlocks, block)
}
}
// GetHelp returns the help text of the command
func GetHelp() string {
return "*Ping*\nPerform a ping to a target like your domain, your public IP or to check if your server is reachable.\n*Examples:*\n`/ping example.com`\n`/ping 1.1.1.1`\n`/ping 2a03:4000:20:16::1`\nAttention: This program uses the local system resolver and does cache the DNS response."
}
// GetStringResponse fetches the vendor data of a given MAC address
func GetStringResponse(target string) string {
// Determine sort of given target
if govalidator.IsIPv4(target) {
// We have an easteregg for you
if target == "127.0.0.1" {
return "*There is no place like 127.0.0.1*"
} else if isPrivateIP(net.ParseIP(target)) {
return "*Not allowed*\nYou have no permission to ping a private IP address."
} else {
out, err := exec.Command("sh", "-c", "ping -c 3 -i 0.2 -W1 -n "+target).Output()
if err != nil {
return err.Error()
}
return "*Ping to " + target + "*\n\n```\n" + string(out) + "\n```"
}
} else if govalidator.IsIPv6(target) {
out, err := exec.Command("sh", "-c", "ping6 -c 2 -i 0.2 "+target).Output()
if err != nil {
return err.Error()
}
return "*Ping6 to " + target + "*\n\n```\n" + string(out) + "\n```"
} else if govalidator.IsDNSName(target) {
out, err := exec.Command("sh", "-c", "ping -c 3 -i 0.2 -W1 -n "+target).Output()
if err != nil {
return err.Error()
}
return "*Domain ping to " + target + "*\n\n```\n" + string(out) + "\n```"
}
return "*Target is not valid*\nPlease provide a valid IPv4, IPv6 or domain name."
}
func isPrivateIP(ip net.IP) bool {
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}
package stats
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"os/signal"
"sort"
"strconv"
"sync"
"syscall"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
)
type MessageStat struct {
Chat map[int64]SingleChat
rwLock sync.RWMutex
}
type SingleChat struct {
User map[int]SingleUser
}
type SingleUser struct {
Message uint64
Date time.Time
Username string
}
var MessageData MessageStat
// Set data based on SingleUser data
func (s *MessageStat) Set(chat int64, user int, username string, val SingleUser) {
s.rwLock.Lock()
defer s.rwLock.Unlock()
s.Chat[chat].User[user] = val
}
// Increase message count of user and also update date and username
func (s *MessageStat) Increase(chat int64, user int, username string) {
s.rwLock.Lock()
defer s.rwLock.Unlock()
// Check if parent element exist and create it if not
if _, ok := s.Chat[chat]; !ok {
s.Chat[chat] = SingleChat{
User: map[int]SingleUser{
user: SingleUser{
Message: 1,
Date: time.Now(),
Username: username,
},
},
}
} else {
s.Chat[chat].User[user] = SingleUser{
Message: s.Chat[chat].User[user].Message + 1,
Date: time.Now(),
Username: username,
}
}
}
// GetChat gets the entire chat dataset
func (s *MessageStat) GetChat(chat int64) (SingleChat, bool) {
s.rwLock.RLock()
defer s.rwLock.RUnlock()
val, found := s.Chat[chat]
return val, found
}
// GetAll gets the entire chat dataset
func (s *MessageStat) GetAll() map[int64]SingleChat {
s.rwLock.RLock()
defer s.rwLock.RUnlock()
data := s.Chat
return data
}
func init() {
// Initize empty object
MessageData = MessageStat{
Chat: map[int64]SingleChat{},
}
// Load stats from disk to memory
statsFile, err := ioutil.ReadFile(`stats.json`)
if err != nil {
fmt.Println(err)
return
}
if err := json.Unmarshal(statsFile, &MessageData.Chat); err != nil {
fmt.Println(err)
return
}
// Save data on program exit
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
WriteData()
}()
// Periodic save of data to disk
go func() {
for {
time.Sleep(time.Minute * 5)
WriteData()
}
}()
}
// WriteData writes the current data to stats.json
func WriteData() {
stats, _ := json.Marshal(MessageData.GetAll())
err := ioutil.WriteFile(`stats.json`, stats, 0644)
if err != nil {
fmt.Println(err)
}
}
// A data structure to hold key/value pairs
type pair struct {
Key int
Value uint64
}
// A slice of pairs that implements sort.Interface to sort by values
type pairList []pair
func (p pairList) Len() int { return len(p) }
func (p pairList) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p pairList) Less(i, j int) bool { return p[i].Value < p[j].Value }
// GetStringResponse fetches gets the toplist of chat users in the chat
// https://github.com/Thomas2500/uTeleBot/blob/master/commands/stats.php
func GetStringResponse(chatID int64) []string {
go WriteData()
text := "*CHAT TOPLIST*\n"
// create list ordered by messages
chatData, ok := MessageData.GetChat(chatID)
if !ok {
return []string{"No valid statistics found"}
}
chatPairList := make(pairList, len(chatData.User))
i := 0
for k, v := range chatData.User {
chatPairList[i] = pair{k, v.Message}
i++
}
sort.Sort(chatPairList)
summaryMessagesTotal := uint64(0)
summaryActiveLastHour := 0
summaryActiveLastDay := 0
index := 1
for _, k := range chatPairList {
text += "*#" + strconv.Itoa(index) + "* " + chatData.User[k.Key].Username + ": " + strconv.FormatUint(chatData.User[k.Key].Message, 10) + "\n"
index++
summaryMessagesTotal += chatData.User[k.Key].Message
if time.Now().Sub(chatData.User[k.Key].Date) <= time.Hour {
summaryActiveLastHour++
summaryActiveLastDay++
} else if time.Now().Sub(chatData.User[k.Key].Date) <= time.Hour*24 {
summaryActiveLastDay++
}
}
return []string{text, "*Summary*\n" + strconv.FormatUint(summaryMessagesTotal, 10) + " messages sent\n" + strconv.Itoa(summaryActiveLastHour) + " users active within the last hour\n" + strconv.Itoa(summaryActiveLastDay) + " users active within the last 24 hours"}
}
// AddRecord increases the message count
func AddRecord(update tgbotapi.Update) {
username := update.Message.From.UserName
if len(username) == 0 {
username = update.Message.From.FirstName + " " + update.Message.From.LastName
}
MessageData.Increase(update.Message.Chat.ID, update.Message.From.ID, username)
}
// GetHelp returns the help text of the command
func GetHelp() string {
return "*Statistics*\nGet statistics of the current chat with `/stats`.\n"
}
package traceroutecomm
import (
"net"
"os/exec"
govalidator "github.com/asaskevich/govalidator"
)
var privateIPBlocks []*net.IPNet
func init() {
for _, cidr := range []string{
"127.0.0.0/8", // IPv4 loopback
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
"::1/128", // IPv6 loopback
"fe80::/10", // IPv6 link-local
"fd00::/8", // IPv6 private address space
} {
_, block, _ := net.ParseCIDR(cidr)
privateIPBlocks = append(privateIPBlocks, block)
}
}
// GetHelp returns the help text of the command
func GetHelp() string {
return "*Traceroute*\nPerform a traceroute to a target like your domain, your public IP or to check if your server is reachable.\n*Examples:*\n`/traceroute example.com`\n`/traceroute 1.1.1.1`\nAttention: This program uses the local system resolver and does cache the DNS response."
}
// GetStringResponse fetches the vendor data of a given MAC address
func GetStringResponse(target string) string {
// Determine sort of given target
if govalidator.IsIPv4(target) {
// We have an easteregg for you
if target == "127.0.0.1" {
return "*There is no place like 127.0.0.1*"
} else if isPrivateIP(net.ParseIP(target)) {
return "*Not allowed*\nYou have no permission to ping a private IP address."
} else {
out, err := exec.Command("sh", "-c", "traceroute -n -w 3 -q 2 -N 32 "+target).Output()
if err != nil {
return err.Error()
}
return "*Traceroute to " + target + "*\n\n```\n" + string(out) + "\n```"
}
} else if govalidator.IsIPv6(target) {
out, err := exec.Command("sh", "-c", "traceroute6 -n -w 3 -q 2 -N 32 "+target).Output()
if err != nil {
return err.Error()
}
return "*Traceroute6 to " + target + "*\n\n```\n" + string(out) + "\n```"
} else if govalidator.IsDNSName(target) {
out, err := exec.Command("sh", "-c", "traceroute -n -w 3 -q 2 -N 32 "+target).Output()
if err != nil {
return err.Error()
}
return "*Domain traceroute to " + target + "*\n\n```\n" + string(out) + "\n```"
}
return "*Target is not valid*\nPlease provide a valid IPv4, IPv6 or domain name."
}
func isPrivateIP(ip net.IP) bool {
for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}
return false
}