diff --git a/hw09_struct_validator/rules/errors.go b/hw09_struct_validator/rules/errors.go index 0b545ee..c8ccd97 100644 --- a/hw09_struct_validator/rules/errors.go +++ b/hw09_struct_validator/rules/errors.go @@ -16,10 +16,6 @@ var ( var ( // программные ошибки функций валидации. - // // правило применимо только к строкам - // ErrOnlyStringRule = errors.New("rule applies only to the string") - // // правило применимо только к целым - // ErrOnlyIntRule = errors.New("rule applies only to the int") // недопустимое условие для правила. ErrInvalidCond = errors.New("invalid condition for the rule") // ошибка компиляции регулярного выражения. @@ -40,7 +36,7 @@ var ( var ( // целое не может быть меньше условия. ErrIntCantBeLess = errors.New("cannot be less") - // целое не может быть меньше больше. + // целое не может быть больше условия. ErrIntCantBeGreater = errors.New("cannot be greater") // целое на входит в список. ErrIntNotInList = errors.New("int is not in the list") diff --git a/hw09_struct_validator/validator.go b/hw09_struct_validator/validator.go index 24f7336..267cf23 100644 --- a/hw09_struct_validator/validator.go +++ b/hw09_struct_validator/validator.go @@ -7,18 +7,6 @@ import ( r "github.com/DimVlas/otus_hw/hw09_struct_validator/rules" ) -// implemented in errors.go file -// type ValidationError struct { -// Field string -// Err error -// } - -// type ValidationErrors []ValidationError - -// func (v ValidationErrors) Error() string { -// panic("implement me") -// } - func Validate(v interface{}) error { // nothing to validate if v == nil { diff --git a/hw09_struct_validator/validator_test.go b/hw09_struct_validator/validator_test.go index 92d33eb..d3d8c9f 100644 --- a/hw09_struct_validator/validator_test.go +++ b/hw09_struct_validator/validator_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" ) -// Test the function on different structures and other types. type ( UserRole string diff --git a/hw11_telnet_client/.sync b/hw11_telnet_client/.sync deleted file mode 100644 index e69de29..0000000 diff --git a/hw11_telnet_client/go.mod b/hw11_telnet_client/go.mod index 839d5b8..ad27adc 100644 --- a/hw11_telnet_client/go.mod +++ b/hw11_telnet_client/go.mod @@ -1,4 +1,4 @@ -module github.com/fixme_my_friend/hw11_telnet_client +module github.com/DimVlas/otus_hw/hw11_telnet_client go 1.22 diff --git a/hw11_telnet_client/main.go b/hw11_telnet_client/main.go index 307acaf..b722012 100644 --- a/hw11_telnet_client/main.go +++ b/hw11_telnet_client/main.go @@ -1,6 +1,92 @@ package main +import ( + "errors" + "fmt" + "log" + "os" + "os/signal" + "path" + "syscall" + "time" +) + +var ( + ErrParseArgs = errors.New("parse args error") + // соединение не установлено. + ErrConnNotEstablish = errors.New("connection is not established") + // завершение клиента. + ErrClientExit = errors.New("connection was closed by client") + // завершение сервера. + ErrServerExit = errors.New("connection was closed by peer") +) + func main() { - // Place your code here, - // P.S. Do not rush to throw context down, think think if it is useful with blocking operation? + var ( + timeout time.Duration + host string + port int + ) + + if err := parseArgs(os.Args[1:], &timeout, &host, &port); err != nil { + log.Println(err) + log.Println("Usage: go-telnet [--timeout=] ") + os.Exit(1) + } + + addr := fmt.Sprintf("%s:%d", host, port) + client := NewTelnetClient(addr, timeout, os.Stdin, os.Stdout) + + if err := client.Connect(); err != nil { + log.Fatalln(err) + } + + log.Printf("...Connected to %s", addr) + + c := make(chan os.Signal, 1) + clientErr := make(chan error) + + defer func() { + client.Close() + close(c) + close(clientErr) + }() + + signal.Notify(c, syscall.SIGINT) + + go func() { + err := client.Send() + if err != nil { + clientErr <- err + return + } + + clientErr <- ErrClientExit + }() + + go func() { + err := client.Receive() + if err != nil { + clientErr <- err + return + } + clientErr <- ErrServerExit + }() + + select { + case <-c: + log.Println("...Connection was closed by", path.Base(os.Args[0])) + return + case err := <-clientErr: + if errors.Is(err, ErrClientExit) { + log.Println("...EOF") + return + } + if errors.Is(err, ErrServerExit) { + log.Println("...Connection was closed by peer") + return + } + log.Println(err) + return + } } diff --git a/hw11_telnet_client/params.go b/hw11_telnet_client/params.go new file mode 100644 index 0000000..6cd0285 --- /dev/null +++ b/hw11_telnet_client/params.go @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "regexp" + "strconv" + "time" +) + +func parseArgs(args []string, timeout *time.Duration, host *string, port *int) error { + fs := flag.NewFlagSet("telnet", flag.ContinueOnError) + fs.SetOutput(bytes.NewBuffer(nil)) + + fs.DurationVar(timeout, "timeout", 10*time.Second, "Connection timeout") + if err := fs.Parse(args); err != nil { + return fmt.Errorf("%w: %w", ErrParseArgs, err) + } + + if fs.NArg() != 2 { + return fmt.Errorf("%w: %w", ErrParseArgs, errors.New("not enough arguments")) + } + + h := fs.Arg(0) + b, _ := regexp.MatchString(`^[A-Za-z0-9-.]+$`, h) + if !b { + return fmt.Errorf("%w %w", ErrParseArgs, errors.New("host: string \""+h+"\" cannot be a host name or address")) + } + *host = h + + p, err := strconv.Atoi(fs.Arg(1)) + if err != nil { + return fmt.Errorf("%w %w", ErrParseArgs, fmt.Errorf("port: %w", err)) + } + *port = p + + return nil +} diff --git a/hw11_telnet_client/telnet.go b/hw11_telnet_client/telnet.go index 369caa1..bbe86f6 100644 --- a/hw11_telnet_client/telnet.go +++ b/hw11_telnet_client/telnet.go @@ -1,7 +1,10 @@ package main import ( + "bufio" + "errors" "io" + "net" "time" ) @@ -13,9 +16,70 @@ type TelnetClient interface { } func NewTelnetClient(address string, timeout time.Duration, in io.ReadCloser, out io.Writer) TelnetClient { - // Place your code here. + return &telnetClient{ + addr: address, + timeout: timeout, + in: in, + out: out, + } +} + +type telnetClient struct { + timeout time.Duration + addr string + conn net.Conn + in io.ReadCloser + out io.Writer +} + +func (tc *telnetClient) Connect() (err error) { + tc.conn, err = net.DialTimeout("tcp", tc.addr, tc.timeout) + return +} + +func (tc *telnetClient) Close() error { + if tc.conn != nil { + return tc.conn.Close() + } return nil } -// Place your code here. -// P.S. Author's solution takes no more than 50 lines. +func (tc *telnetClient) Send() error { + if tc.conn == nil { + return ErrConnNotEstablish + } + + scanner := bufio.NewScanner(tc.in) + for scanner.Scan() { + mess := append(scanner.Bytes(), '\n') + if _, err := tc.conn.Write(mess); err != nil { + return err + } + } + if err := scanner.Err(); err != nil { + return err + } + + return nil +} + +func (tc *telnetClient) Receive() error { + if tc.conn == nil { + return ErrConnNotEstablish + } + + reader := bufio.NewReader(tc.conn) + for { + message, err := reader.ReadBytes('\n') + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + _, err = tc.out.Write(message) + if err != nil { + return err + } + } +} diff --git a/hw11_telnet_client/telnet_test.go b/hw11_telnet_client/telnet_test.go index fb89f6d..816e09c 100644 --- a/hw11_telnet_client/telnet_test.go +++ b/hw11_telnet_client/telnet_test.go @@ -62,4 +62,151 @@ func TestTelnetClient(t *testing.T) { wg.Wait() }) + + t.Run("bad_connect", func(t *testing.T) { + in := &bytes.Buffer{} + out := &bytes.Buffer{} + client := NewTelnetClient("localhost:4242", 1*time.Second, io.NopCloser(in), out) + + require.EqualError(t, client.Connect(), "dial tcp [::1]:4242: connect: connection refused") + }) + + t.Run("without_connect", func(t *testing.T) { + in := &bytes.Buffer{} + out := &bytes.Buffer{} + client := NewTelnetClient("localhost:4242", 5*time.Second, io.NopCloser(in), out) + + require.ErrorIs(t, client.Send(), ErrConnNotEstablish) + require.ErrorIs(t, client.Receive(), ErrConnNotEstablish) + require.NoError(t, client.Close()) + }) + + t.Run("close_listen", func(t *testing.T) { + l, err := net.Listen("tcp", "127.0.0.1:") + require.NoError(t, err) + + in := &bytes.Buffer{} + out := &bytes.Buffer{} + + timeout, err := time.ParseDuration("1s") + require.NoError(t, err) + + client := NewTelnetClient(l.Addr().String(), timeout, io.NopCloser(in), out) + require.NoError(t, client.Connect()) + defer func() { require.NoError(t, client.Close()) }() + + l.Close() + + conn := client.(*telnetClient).conn + + in.WriteString("hello\n") + require.EqualError(t, client.Send(), + "write tcp "+conn.LocalAddr().String()+"->"+conn.RemoteAddr().String()+": write: connection reset by peer") + + // при получении возращает EOF, который перехватывается т.к. трактуется как корректное окончание + require.NoError(t, client.Receive()) + }) +} + +var testParamData = []struct { + name string + args []string + timeout time.Duration + host string + port int + err string +}{ + { + name: "parseArgs_success", + args: []string{ + "--timeout=5s", + "localhost", + "12345", + }, + timeout: 5 * time.Second, + host: "localhost", + port: 12345, + err: "", + }, + { + name: "parseArgs_success_without_timeout", + args: []string{ + "localhost", + "12345", + }, + timeout: 10 * time.Second, + host: "localhost", + port: 12345, + err: "", + }, + { + name: "parseArgs_bad_timeout", + args: []string{ + "--timeout=5ass", + "localhost", + "12345", + }, + timeout: 5 * time.Second, + host: "", + port: 0, + err: `parse args error: invalid value "5ass" for flag -timeout: parse error`, + }, + { + name: "parseArgs_bad_args_count", + args: []string{ + "--timeout=5s", + "localhost", + }, + timeout: 5 * time.Second, + host: "", + port: 0, + err: "parse args error: not enough arguments", + }, + { + name: "parseArgs_invalid_host", + args: []string{ + "--timeout=5s", + "domain/localhost", + "123", + }, + timeout: 5 * time.Second, + host: "", + port: 0, + err: "parse args error host: string \"domain/localhost\" cannot be a host name or address", + }, + { + name: "parseArgs_invalid_port", + args: []string{ + "--timeout=5s", + "localhost", + "a123", + }, + timeout: 5 * time.Second, + host: "localhost", + port: 0, + err: "parse args error port: strconv.Atoi: parsing \"a123\": invalid syntax", + }, +} + +func TestParseArgs(t *testing.T) { + for _, data := range testParamData { + t.Run(data.name, func(t *testing.T) { + var ( + timeout time.Duration + host string + port int + ) + + err := parseArgs(data.args, &timeout, &host, &port) + + if data.err != "" { + require.EqualError(t, err, data.err) + } else { + require.NoError(t, err) + } + + require.Equal(t, data.host, host) + require.Equal(t, data.port, port) + }) + } }