- UDP prijemnik (podrazumevano 46465, NE 46464 gde uredjaji slusaju discovery): dnevnik rada sa zaglavljem #TERMINIA, heartbeat JSON, sve ostalo se odbacuje - paket bez zaglavlja (FW < 0.13.4) se vodi kao "nepoznat uredjaj" po izvornoj IP adresi i tako je i oznacen u prikazu - dnevnik u fajlove po uredjaju i danu, rotacija po velicini + tri kvote (starost, po uredjaju, ukupno); otvoren fajl se nikad ne brise - heartbeat u memoriji; interval se meri iz razmaka javljanja, "tih" = 3x interval - web stranica (podrazumevano 127.0.0.1:8081): spisak uredjaja, crveno za tihe, poslednjih N linija sa pretragom, /dnevnik.txt i /api/uredjaji - bez baze i bez spoljnih zavisnosti; ne dira licencni servis, njegovu bazu, rute ni nginx - sanitizacija svega iz paketa (duzine, kontrolni znaci, ime foldera) — bez izlaska iz foldera dnevnika - systemd unit + primer env fajla + README sa uputstvom za pustanje na server - testovi: parsiranje (zaglavlje/JSON/bez zaglavlja/smece), registar, rotacija i kvote, web rute Nije pusteno na server — puštanje radi vlasnik po logger/README.md.
214 lines
6.4 KiB
Go
214 lines
6.4 KiB
Go
package logger
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseLogSaZaglavljem(t *testing.T) {
|
|
raw := "#TERMINIA TERM-001 1C:DB:D4:A6:A4:60 0.13.4\n" +
|
|
"[stampac] opet se javlja na 192.168.123.100 (ping)\n" +
|
|
"[izvestaj] dan 20659: 14 karata, 2100 RSD\n"
|
|
p := ParsePacket([]byte(raw), "192.168.0.20")
|
|
|
|
if p.Kind != KindLog {
|
|
t.Fatalf("vrsta = %v, ocekivano KindLog", p.Kind)
|
|
}
|
|
if !p.Identified {
|
|
t.Error("paket sa zaglavljem mora biti identifikovan")
|
|
}
|
|
if p.Name != "TERM-001" || p.MAC != "1C:DB:D4:A6:A4:60" || p.FW != "0.13.4" {
|
|
t.Errorf("identitet = %q / %q / %q", p.Name, p.MAC, p.FW)
|
|
}
|
|
if len(p.Lines) != 2 {
|
|
t.Fatalf("linija = %d, ocekivano 2: %q", len(p.Lines), p.Lines)
|
|
}
|
|
if !strings.HasPrefix(p.Lines[0], "[stampac]") {
|
|
t.Errorf("prva linija = %q", p.Lines[0])
|
|
}
|
|
if got := DeviceKey(p.MAC, p.SourceIP); got != "1CDBD4A6A460" {
|
|
t.Errorf("kljuc = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestParseZaglavljeSaRazmakomUNazivu(t *testing.T) {
|
|
// Naziv uredjaja kuca korisnik i sme da ima razmake.
|
|
p := ParsePacket([]byte("#TERMINIA Bazen Ulaz A 1c-db-d4-a6-a4-60 0.13.4\nzdravo\n"), "10.0.0.5")
|
|
if p.Name != "Bazen Ulaz A" {
|
|
t.Errorf("naziv = %q", p.Name)
|
|
}
|
|
if p.MAC != "1C:DB:D4:A6:A4:60" {
|
|
t.Errorf("mac = %q", p.MAC)
|
|
}
|
|
if p.FW != "0.13.4" {
|
|
t.Errorf("fw = %q", p.FW)
|
|
}
|
|
}
|
|
|
|
func TestParseHeartbeat(t *testing.T) {
|
|
raw := `{"app":"TERMINIA","name":"TERM-001","mac":"1C:DB:D4:A6:A4:60",` +
|
|
`"ip":"192.168.0.20","fw":"0.13.4","serial":"1CDBD4A6A460","hb":1}`
|
|
p := ParsePacket([]byte(raw), "192.168.0.20")
|
|
|
|
if p.Kind != KindHeartbeat {
|
|
t.Fatalf("vrsta = %v, ocekivano KindHeartbeat", p.Kind)
|
|
}
|
|
if !p.Identified || p.MAC != "1C:DB:D4:A6:A4:60" || p.IP != "192.168.0.20" ||
|
|
p.FW != "0.13.4" || p.Serial != "1CDBD4A6A460" || p.Name != "TERM-001" {
|
|
t.Errorf("identitet = %+v", p)
|
|
}
|
|
if len(p.Lines) != 0 {
|
|
t.Errorf("heartbeat ne sme imati linije: %q", p.Lines)
|
|
}
|
|
}
|
|
|
|
func TestParseHeartbeatSaIdPoljem(t *testing.T) {
|
|
// Stariji/drugaciji klijent salje serijski pod imenom "id".
|
|
p := ParsePacket([]byte(`{"name":"T","mac":"AABBCCDDEEFF","id":"XYZ","hb":1}`), "10.0.0.1")
|
|
if p.Kind != KindHeartbeat || p.Serial != "XYZ" {
|
|
t.Errorf("rezultat = %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestParseBezZaglavlja(t *testing.T) {
|
|
// FW stariji od 0.13.4: goli tekst, znamo samo izvornu adresu.
|
|
p := ParsePacket([]byte("nesto se desilo\ndrugi red\n"), "192.168.0.77")
|
|
if p.Kind != KindLog {
|
|
t.Fatalf("vrsta = %v", p.Kind)
|
|
}
|
|
if p.Identified {
|
|
t.Error("paket bez zaglavlja NE SME da vazi za identifikovan")
|
|
}
|
|
if p.MAC != "" {
|
|
t.Errorf("mac = %q, ocekivano prazno", p.MAC)
|
|
}
|
|
key := DeviceKey(p.MAC, p.SourceIP)
|
|
if key != "nepoznat-192_168_0_77" {
|
|
t.Errorf("kljuc = %q", key)
|
|
}
|
|
}
|
|
|
|
func TestParseSmeceNeObaraServis(t *testing.T) {
|
|
slucajevi := [][]byte{
|
|
nil,
|
|
{},
|
|
[]byte("\n\n\n"),
|
|
[]byte("{"), // krnj JSON
|
|
[]byte(`{"app":"TERMINIA"`), // presecen paket
|
|
[]byte(`{"nesto":"tudje"}`), // JSON bez identiteta
|
|
{0x00, 0x01, 0x02, 0xff, 0xfe}, // binarno smece
|
|
[]byte("#TERMINIA\n"), // zaglavlje bez ijednog polja
|
|
[]byte(strings.Repeat("A", MaxPacketBytes*3)), // predugacak paket
|
|
}
|
|
for i, c := range slucajevi {
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("slucaj %d: panika: %v", i, r)
|
|
}
|
|
}()
|
|
p := ParsePacket(c, "1.2.3.4")
|
|
// Bilo koji ishod je prihvatljiv osim pada; ako je nesto proslo,
|
|
// mora biti u granicama.
|
|
if len(p.Lines) > MaxLinesPerPacket {
|
|
t.Errorf("slucaj %d: previse linija %d", i, len(p.Lines))
|
|
}
|
|
for _, ln := range p.Lines {
|
|
if len([]rune(ln)) > MaxLineLen {
|
|
t.Errorf("slucaj %d: linija duza od granice", i)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestParseSeceIKontrolneZnake(t *testing.T) {
|
|
dugacko := strings.Repeat("x", 1000)
|
|
raw := "#TERMINIA " + strings.Repeat("N", 200) + " 1C:DB:D4:A6:A4:60 " + strings.Repeat("9", 100) + "\n" +
|
|
"linija\x1b[31m sa ESC \x07 i \x00 nulom\n" + dugacko + "\n"
|
|
p := ParsePacket([]byte(raw), "1.2.3.4")
|
|
|
|
if len([]rune(p.Name)) > MaxNameLen {
|
|
t.Errorf("naziv nije skracen: %d", len([]rune(p.Name)))
|
|
}
|
|
if len([]rune(p.FW)) > MaxFWLen {
|
|
t.Errorf("fw nije skracen: %d", len([]rune(p.FW)))
|
|
}
|
|
if strings.ContainsAny(p.Lines[0], "\x1b\x07\x00") {
|
|
t.Errorf("kontrolni znaci nisu uklonjeni: %q", p.Lines[0])
|
|
}
|
|
if len([]rune(p.Lines[1])) > MaxLineLen {
|
|
t.Errorf("linija nije skracena: %d", len([]rune(p.Lines[1])))
|
|
}
|
|
}
|
|
|
|
func TestParsePreviseLinija(t *testing.T) {
|
|
var b strings.Builder
|
|
b.WriteString("#TERMINIA T 1C:DB:D4:A6:A4:60 0.13.4\n")
|
|
for i := 0; i < 500; i++ {
|
|
b.WriteString("linija\n")
|
|
}
|
|
p := ParsePacket([]byte(b.String()), "1.2.3.4")
|
|
if len(p.Lines) != MaxLinesPerPacket {
|
|
t.Errorf("linija = %d, ocekivano %d", len(p.Lines), MaxLinesPerPacket)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeMAC(t *testing.T) {
|
|
dobri := map[string]string{
|
|
"1C:DB:D4:A6:A4:60": "1C:DB:D4:A6:A4:60",
|
|
"1c:db:d4:a6:a4:60": "1C:DB:D4:A6:A4:60",
|
|
"1cdbd4a6a460": "1C:DB:D4:A6:A4:60",
|
|
"1C-DB-D4-A6-A4-60": "1C:DB:D4:A6:A4:60",
|
|
}
|
|
for in, want := range dobri {
|
|
got, ok := NormalizeMAC(in)
|
|
if !ok || got != want {
|
|
t.Errorf("NormalizeMAC(%q) = %q,%v", in, got, ok)
|
|
}
|
|
}
|
|
losi := []string{"", "1C:DB:D4:A6:A4", "1C:DB:D4:A6:A4:6G", "../../etc/passwd",
|
|
strings.Repeat("A", 200), "1C:DB:D4:A6:A4:60:70"}
|
|
for _, in := range losi {
|
|
if _, ok := NormalizeMAC(in); ok {
|
|
t.Errorf("NormalizeMAC(%q) je prosao, a ne bi smeo", in)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSanitizeSegmentNemaIzlaskaIzFoldera(t *testing.T) {
|
|
slucajevi := map[string]string{
|
|
"../../etc/passwd": "etc_passwd",
|
|
"/apsolutno/putanja": "apsolutno_putanja",
|
|
"C:\\Windows\\win": "C__Windows_win",
|
|
"": "nepoznat",
|
|
"...": "nepoznat",
|
|
"1CDBD4A6A460": "1CDBD4A6A460",
|
|
}
|
|
for in, want := range slucajevi {
|
|
got := SanitizeSegment(in)
|
|
if got != want {
|
|
t.Errorf("SanitizeSegment(%q) = %q, ocekivano %q", in, got, want)
|
|
}
|
|
if strings.ContainsAny(got, `/\`) || got == ".." {
|
|
t.Errorf("SanitizeSegment(%q) = %q — pusta izlazak iz foldera", in, got)
|
|
}
|
|
}
|
|
if n := len(SanitizeSegment(strings.Repeat("a", 500))); n > MaxKeyLen {
|
|
t.Errorf("kljuc nije ogranicen: %d", n)
|
|
}
|
|
}
|
|
|
|
func TestDeviceKeyPoIPKadNemaMAC(t *testing.T) {
|
|
if got := DeviceKey("", "10.20.30.40"); got != "nepoznat-10_20_30_40" {
|
|
t.Errorf("kljuc = %q", got)
|
|
}
|
|
if got := DeviceKey("", ""); got != "nepoznat-bez-adrese" {
|
|
t.Errorf("kljuc bez adrese = %q", got)
|
|
}
|
|
// Lazna izvorna adresa ne sme da napravi putanju.
|
|
if got := DeviceKey("", "../../root"); strings.ContainsAny(got, `/\`) {
|
|
t.Errorf("kljuc = %q", got)
|
|
}
|
|
}
|