-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #30 from ifanchu/topic/ifanchu/parse_logins_bad
Issue 29: Parse logins.bad
- Loading branch information
Showing
4 changed files
with
247 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
[01/01/2021 10:15:16 Fri] 172.22.0.1 |