Skip to content

Commit

Permalink
Merge pull request #30 from ifanchu/topic/ifanchu/parse_logins_bad
Browse files Browse the repository at this point in the history
Issue 29: Parse logins.bad
  • Loading branch information
PichuChen authored Jan 1, 2021
2 parents aaeead5 + 3f65d49 commit fd1f00b
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 0 deletions.
163 changes: 163 additions & 0 deletions bad_logins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package bbs

import (
"bufio"
"errors"
"fmt"
"log"
"os"
"strings"
"time"
)

// logins.bad 有兩種,一個在BBSHOME,一個在User下面
// https://github.com/ptt/pttbbs/blob/master/include/common.h#L56
// https://github.com/ptt/pttbbs/blob/master/common/bbs/passwd.c#L255
//
// BBSHOME/logins.bad: 這個檔裡有每個 user的login attempt且包含成功與失敗。第一個字元若是"-"代表失敗。
//
// test03 [01/01/2021 10:11:45 Fri] [email protected]
// test04 [01/01/2021 10:13:35 Fri] [email protected]
// test05 [01/01/2021 10:13:45 Fri] [email protected]
// SYSOP [01/01/2021 10:13:53 Fri] [email protected]
// test06 [01/01/2021 10:14:38 Fri] [email protected]
// SYSOP [01/01/2021 10:14:46 Fri] [email protected]
// -test01 [01/01/2021 10:15:16 Fri] [email protected]
// -test02 [01/01/2021 10:15:19 Fri] [email protected]
// -test03 [01/01/2021 10:15:22 Fri] [email protected]
// test04 [01/01/2021 10:15:38 Fri] [email protected]
//
// BBSHOME/home/<x>/<user>/logins.bad: 這個檔裡只有該user的 失敗 login attempt
//
// ╰─➤ cat home/T/test01/logins.bad
// [01/01/2021 10:15:16 Fri] 172.22.0.1
//
// 目前想法是用同一個struct來parse這2種logins.bad
//
// type LoginAttempt struct {
// Success bool
// UserId string
// LoginStartTime time.Time
// FromHost string
// }
// For BBSHOME/logins.bad ,這個檔裡四個field都有,所以沒問題。
// 但在user/logins.bad,缺少 UserId ,所以parse出來的struct就沒有 UserId,需要caller assign

const (
// UserIdLength is fixed to 12
UserIdLength = 12
// FromHostPrefix is a prefix affixed to ip only in BBSHOME/logins.bad
fromHostPrefix = "?@"
loginStartTimeFormatString = "[01/02/2006 15:04:05 Mon]"
)

var (
InvalidLoginsBadFormat = errors.New("Invalid logins.bad line format")
)

// LoginAttempt represents an entry in logins.bad file to indicate a successful or failed login
// attempt for a UserId. Note that UserId could be empty if the logins.bad is under user dir.
type LoginAttempt struct {
Success bool
UserId string
LoginStartTime time.Time
FromHost string
}

// OpenBadLoginFile opens logins.bad file and returns a slice of LoginAttempt.
// Note that depending on different format of logins.bad as descirbed above, each LoginAttempt
// might not have LoginAttempt.UserId field
func OpenBadLoginFile(filename string) ([]*LoginAttempt, error) {
file, err := os.Open(filename)
if err != nil {
log.Println(err)
return nil, err
}

var ret []*LoginAttempt

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Bytes()
a := &LoginAttempt{}
err = a.UnmarshalText(line)
if err != nil {
return nil, err
}
ret = append(ret, a)
}
return ret, nil
}

// UnmarshalText implements encoding.TextUnmarshaler to unmarshal text to the receiver
func (l *LoginAttempt) UnmarshalText(text []byte) error {
str := string(text)

idx := 0 // current index of str
// Handle Success and UserId
switch str[idx] {
case ' ':
idx += 1
l.Success = true
// Next 12 is UserId
l.UserId = str[idx : idx+UserIdLength]
idx += UserIdLength
case '-':
idx += 1
l.Success = false
l.UserId = str[idx : idx+UserIdLength]
idx += UserIdLength
case '[':
// This indicates this line has no Success and UserId, set Success to false
l.Success = false
l.UserId = ""
default:
return InvalidLoginsBadFormat
}
l.UserId = strings.TrimSpace(l.UserId)
// Now idx points to the start of time
// TODO: do we need to consider timezone? This Parse returns UTC
t, err := time.Parse(loginStartTimeFormatString, str[idx:idx+len(loginStartTimeFormatString)])
if err != nil {
return err
}
l.LoginStartTime = t
idx += len(loginStartTimeFormatString)

l.FromHost = strings.TrimLeft(str[idx+1:], fromHostPrefix)
return nil
}

// MarshalText implements encoding.TextMarshaler to marshal receiver to text
func (l *LoginAttempt) MarshalText() ([]byte, error) {
var sb strings.Builder
if l.IsUnderBbsHome() {
if l.Success {
sb.WriteRune(' ')
} else {
sb.WriteRune('-')
}
// Right padding UserId
sb.WriteString(fmt.Sprintf("%-*s", UserIdLength, l.UserId))
}
// time
formatted := ""
// TODO: consider timezone?
formatted = l.LoginStartTime.Format(loginStartTimeFormatString)
sb.WriteString(formatted)
sb.WriteRune(' ')
// ip
if l.IsUnderBbsHome() {
sb.WriteString(fromHostPrefix)
}
sb.WriteString(l.FromHost)

return []byte(sb.String()), nil
}

// IsUnderBbsHome return true if this LoginAttempt was read from logins.bad from under BBSHOME.
// The difference between logins.bad between under BBSHOME and under User Dir is whether it contains
// UserId
func (l *LoginAttempt) IsUnderBbsHome() bool {
return len(l.UserId) > 1
}
68 changes: 68 additions & 0 deletions bad_logins_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package bbs

import (
"testing"
)

func TestOpenBadLoginFile(t *testing.T) {
type testCase struct {
filename string
expected []*LoginAttempt
}
testCases := []*testCase{
{
filename: "testcase/bad_logins/logins.bad",
expected: nil,
},
{
filename: "testcase/bad_logins/test01/logins.bad",
expected: nil,
},
}

for _, c := range testCases {
attemps, err := OpenBadLoginFile(c.filename)
if err != nil {
t.Errorf("Failed to open logins.bad. Err %v", err)
}
for _, l := range attemps {
if l.FromHost == "" {
t.Error("FromHost should never be empty")
}
if l.LoginStartTime.IsZero() {
t.Error("LoginStartTime should not be zero")
}
if l.UserId == "" && l.Success {
t.Error("If UserId is empty, Success must be false")
}
}
}
}

func TestLoginAttempt(t *testing.T) {
testLines := []string{
" SYSOP [01/01/2021 10:08:56 Fri] [email protected]",
"-test03 [01/12/2021 13:14:15 Tue] [email protected]",
" test03 [12/30/2021 21:55:59 Thu] [email protected]",
" abc123456789[01/01/2021 10:11:09 Fri] [email protected]",
"-abc123456789[01/01/2021 10:11:09 Fri] [email protected]",
"[01/01/2021 01:02:03 Fri] 1.2.3.4",
"[01/12/2021 13:14:15 Tue] 255.255.255.255",
"[12/30/2021 21:55:59 Thu] 100.100.100.100",
}

for _, line := range testLines {
attempt := &LoginAttempt{}
err := attempt.UnmarshalText([]byte(line))
if err != nil {
t.Errorf("Failed to unmarshal line %s. Err %v", line, err)
}
formatted, err := attempt.MarshalText()
if err != nil {
t.Errorf("Failed to marshal LoginAttempt. Err %v", err)
}
if string(formatted) != line {
t.Errorf("Marshaled != original. Original: '%s'. Marshaled: '%s'.", line, string(formatted))
}
}
}
15 changes: 15 additions & 0 deletions testcase/bad_logins/logins.bad
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SYSOP [01/01/2021 10:08:56 Fri] [email protected]
SYSOP [01/01/2021 10:10:50 Fri] [email protected]
abc123456789[01/01/2021 10:11:09 Fri] [email protected]
test01 [01/01/2021 10:11:23 Fri] [email protected]
test02 [01/01/2021 10:11:35 Fri] [email protected]
test03 [01/01/2021 10:11:45 Fri] [email protected]
test04 [01/01/2021 10:13:35 Fri] [email protected]
test05 [01/01/2021 10:13:45 Fri] [email protected]
SYSOP [01/01/2021 10:13:53 Fri] [email protected]
test06 [01/01/2021 10:14:38 Fri] [email protected]
SYSOP [01/01/2021 10:14:46 Fri] [email protected]
-test01 [01/01/2021 10:15:16 Fri] [email protected]
-test02 [01/01/2021 10:15:19 Fri] [email protected]
-test03 [01/01/2021 10:15:22 Fri] [email protected]
test04 [01/01/2021 10:15:38 Fri] [email protected]
1 change: 1 addition & 0 deletions testcase/bad_logins/test01/logins.bad
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[01/01/2021 10:15:16 Fri] 172.22.0.1

0 comments on commit fd1f00b

Please sign in to comment.