2019-03-14 11:30:04 +03:00
package integration
import (
"crypto/tls"
2021-11-25 13:10:06 +03:00
"crypto/x509"
"errors"
"fmt"
2024-01-09 19:00:07 +03:00
"io"
2019-03-14 11:30:04 +03:00
"net"
"net/http"
2019-06-07 20:30:07 +03:00
"net/http/httptest"
2019-09-13 21:00:06 +03:00
"strings"
2024-01-09 19:00:07 +03:00
"testing"
2019-03-14 11:30:04 +03:00
"time"
2024-01-09 19:00:07 +03:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
2023-02-03 17:24:05 +03:00
"github.com/traefik/traefik/v3/integration/try"
2019-03-14 11:30:04 +03:00
)
type TCPSuite struct { BaseSuite }
2024-01-09 19:00:07 +03:00
func TestTCPSuite ( t * testing . T ) {
suite . Run ( t , new ( TCPSuite ) )
2019-03-14 11:30:04 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) SetupSuite ( ) {
s . BaseSuite . SetupSuite ( )
s . createComposeProject ( "tcp" )
s . composeUp ( )
}
func ( s * TCPSuite ) TearDownSuite ( ) {
s . BaseSuite . TearDownSuite ( )
}
func ( s * TCPSuite ) TestMixed ( ) {
file := s . adaptFile ( "fixtures/tcp/mixed.toml" , struct {
2022-07-13 19:32:08 +03:00
Whoami string
WhoamiA string
WhoamiB string
WhoamiNoCert string
} {
2024-01-09 19:00:07 +03:00
Whoami : "http://" + s . getComposeServiceIP ( "whoami" ) + ":80" ,
WhoamiA : s . getComposeServiceIP ( "whoami-a" ) + ":8080" ,
WhoamiB : s . getComposeServiceIP ( "whoami-b" ) + ":8080" ,
WhoamiNoCert : s . getComposeServiceIP ( "whoami-no-cert" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "Path(`/test`)" ) )
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Traefik passes through, termination handled by whoami-a
2021-11-25 13:10:06 +03:00
out , err := guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-a.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-a" )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Traefik passes through, termination handled by whoami-b
2021-11-25 13:10:06 +03:00
out , err = guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-b.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-b" )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Termination handled by traefik
2019-03-14 11:30:04 +03:00
out , err = guessWho ( "127.0.0.1:8093" , "whoami-c.test" , true )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-cert" )
2019-03-14 11:30:04 +03:00
tr1 := & http . Transport {
TLSClientConfig : & tls . Config {
InsecureSkipVerify : true ,
} ,
}
req , err := http . NewRequest ( http . MethodGet , "https://127.0.0.1:8093/whoami/" , nil )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
err = try . RequestWithTransport ( req , 10 * time . Second , tr1 , try . StatusCodeIs ( http . StatusOK ) )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
req , err = http . NewRequest ( http . MethodGet , "https://127.0.0.1:8093/not-found/" , nil )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
err = try . RequestWithTransport ( req , 10 * time . Second , tr1 , try . StatusCodeIs ( http . StatusNotFound ) )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
err = try . GetRequest ( "http://127.0.0.1:8093/test" , 500 * time . Millisecond , try . StatusCodeIs ( http . StatusOK ) )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
err = try . GetRequest ( "http://127.0.0.1:8093/not-found" , 500 * time . Millisecond , try . StatusCodeIs ( http . StatusNotFound ) )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestTLSOptions ( ) {
file := s . adaptFile ( "fixtures/tcp/multi-tls-options.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiNoCert string
} {
2024-01-09 19:00:07 +03:00
WhoamiNoCert : s . getComposeServiceIP ( "whoami-no-cert" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2019-06-17 19:14:08 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-06-17 19:14:08 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`whoami-c.test`)" ) )
require . NoError ( s . T ( ) , err )
2019-06-17 19:14:08 +03:00
2022-08-09 18:36:08 +03:00
// Check that we can use a client tls version <= 1.2 with hostSNI 'whoami-c.test'
out , err := guessWhoTLSMaxVersion ( "127.0.0.1:8093" , "whoami-c.test" , true , tls . VersionTLS12 )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-cert" )
2019-06-17 19:14:08 +03:00
2022-08-09 18:36:08 +03:00
// Check that we can use a client tls version <= 1.3 with hostSNI 'whoami-d.test'
out , err = guessWhoTLSMaxVersion ( "127.0.0.1:8093" , "whoami-d.test" , true , tls . VersionTLS13 )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-cert" )
2019-06-17 19:14:08 +03:00
2022-08-09 18:36:08 +03:00
// Check that we cannot use a client tls version <= 1.2 with hostSNI 'whoami-d.test'
_ , err = guessWhoTLSMaxVersion ( "127.0.0.1:8093" , "whoami-d.test" , true , tls . VersionTLS12 )
2024-01-09 19:00:07 +03:00
assert . ErrorContains ( s . T ( ) , err , "protocol version not supported" )
2022-12-06 20:28:05 +03:00
// Check that we can't reach a route with an invalid mTLS configuration.
conn , err := tls . Dial ( "tcp" , "127.0.0.1:8093" , & tls . Config {
ServerName : "whoami-i.test" ,
InsecureSkipVerify : true ,
} )
2024-01-09 19:00:07 +03:00
assert . Nil ( s . T ( ) , conn )
assert . Error ( s . T ( ) , err )
2019-06-17 19:14:08 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestNonTLSFallback ( ) {
file := s . adaptFile ( "fixtures/tcp/non-tls-fallback.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiA string
WhoamiB string
WhoamiNoCert string
WhoamiNoTLS string
} {
2024-01-09 19:00:07 +03:00
WhoamiA : s . getComposeServiceIP ( "whoami-a" ) + ":8080" ,
WhoamiB : s . getComposeServiceIP ( "whoami-b" ) + ":8080" ,
WhoamiNoCert : s . getComposeServiceIP ( "whoami-no-cert" ) + ":8080" ,
WhoamiNoTLS : s . getComposeServiceIP ( "whoami-no-tls" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`*`)" ) )
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Traefik passes through, termination handled by whoami-a
2021-11-25 13:10:06 +03:00
out , err := guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-a.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-a" )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Traefik passes through, termination handled by whoami-b
2021-11-25 13:10:06 +03:00
out , err = guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-b.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-b" )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Termination handled by traefik
2019-03-14 11:30:04 +03:00
out , err = guessWho ( "127.0.0.1:8093" , "whoami-c.test" , true )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-cert" )
2019-03-14 11:30:04 +03:00
out , err = guessWho ( "127.0.0.1:8093" , "" , false )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-tls" )
2019-03-14 11:30:04 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestNonTlsTcp ( ) {
file := s . adaptFile ( "fixtures/tcp/non-tls.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiNoTLS string
} {
2024-01-09 19:00:07 +03:00
WhoamiNoTLS : s . getComposeServiceIP ( "whoami-no-tls" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-03-14 11:30:04 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`*`)" ) )
require . NoError ( s . T ( ) , err )
2019-03-14 11:30:04 +03:00
2019-06-07 20:30:07 +03:00
// Traefik will forward every requests on the given port to whoami-no-tls
2019-03-14 11:30:04 +03:00
out , err := guessWho ( "127.0.0.1:8093" , "" , false )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-no-tls" )
2019-03-14 11:30:04 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestCatchAllNoTLS ( ) {
file := s . adaptFile ( "fixtures/tcp/catch-all-no-tls.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiBannerAddress string
} {
2024-01-09 19:00:07 +03:00
WhoamiBannerAddress : s . getComposeServiceIP ( "whoami-banner" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2019-06-07 20:30:07 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-06-07 20:30:07 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`*`)" ) )
require . NoError ( s . T ( ) , err )
2019-06-07 20:30:07 +03:00
// Traefik will forward every requests on the given port to whoami-no-tls
out , err := welcome ( "127.0.0.1:8093" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "Welcome" )
2019-06-07 20:30:07 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestCatchAllNoTLSWithHTTPS ( ) {
file := s . adaptFile ( "fixtures/tcp/catch-all-no-tls-with-https.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiNoTLSAddress string
WhoamiURL string
} {
2024-01-09 19:00:07 +03:00
WhoamiNoTLSAddress : s . getComposeServiceIP ( "whoami-no-tls" ) + ":8080" ,
WhoamiURL : "http://" + s . getComposeServiceIP ( "whoami" ) + ":80" ,
2022-07-13 19:32:08 +03:00
} )
2019-06-07 20:30:07 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2019-06-07 20:30:07 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`*`)" ) )
require . NoError ( s . T ( ) , err )
2019-06-07 20:30:07 +03:00
req := httptest . NewRequest ( http . MethodGet , "https://127.0.0.1:8093/test" , nil )
req . RequestURI = ""
err = try . RequestWithTransport ( req , 500 * time . Millisecond , & http . Transport {
TLSClientConfig : & tls . Config {
InsecureSkipVerify : true ,
} ,
} , try . StatusCodeIs ( http . StatusOK ) )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2019-06-07 20:30:07 +03:00
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestMiddlewareAllowList ( ) {
file := s . adaptFile ( "fixtures/tcp/ip-allowlist.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiA string
WhoamiB string
} {
2024-01-09 19:00:07 +03:00
WhoamiA : s . getComposeServiceIP ( "whoami-a" ) + ":8080" ,
WhoamiB : s . getComposeServiceIP ( "whoami-b" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2021-06-11 16:30:05 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2021-06-11 16:30:05 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`whoami-a.test`)" ) )
require . NoError ( s . T ( ) , err )
2021-06-11 16:30:05 +03:00
2022-10-26 18:16:05 +03:00
// Traefik not passes through, ipAllowList closes connection
2021-11-25 13:10:06 +03:00
_ , err = guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-a.test" )
2024-01-09 19:00:07 +03:00
assert . ErrorIs ( s . T ( ) , err , io . EOF )
2021-06-11 16:30:05 +03:00
// Traefik passes through, termination handled by whoami-b
2021-11-25 13:10:06 +03:00
out , err := guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-b.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-b" )
2021-06-11 16:30:05 +03:00
}
2024-01-11 12:40:06 +03:00
func ( s * TCPSuite ) TestMiddlewareWhiteList ( ) {
file := s . adaptFile ( "fixtures/tcp/ip-whitelist.toml" , struct {
WhoamiA string
WhoamiB string
} {
WhoamiA : s . getComposeServiceIP ( "whoami-a" ) + ":8080" ,
WhoamiB : s . getComposeServiceIP ( "whoami-b" ) + ":8080" ,
} )
s . traefikCmd ( withConfigFile ( file ) )
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`whoami-a.test`)" ) )
require . NoError ( s . T ( ) , err )
// Traefik not passes through, ipWhiteList closes connection
_ , err = guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-a.test" )
assert . ErrorIs ( s . T ( ) , err , io . EOF )
// Traefik passes through, termination handled by whoami-b
out , err := guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-b.test" )
require . NoError ( s . T ( ) , err )
assert . Contains ( s . T ( ) , out , "whoami-b" )
}
2024-01-09 19:00:07 +03:00
func ( s * TCPSuite ) TestWRR ( ) {
file := s . adaptFile ( "fixtures/tcp/wrr.toml" , struct {
2022-07-13 19:32:08 +03:00
WhoamiB string
WhoamiAB string
} {
2024-01-09 19:00:07 +03:00
WhoamiB : s . getComposeServiceIP ( "whoami-b" ) + ":8080" ,
WhoamiAB : s . getComposeServiceIP ( "whoami-ab" ) + ":8080" ,
2022-07-13 19:32:08 +03:00
} )
2021-11-25 13:10:06 +03:00
2024-01-09 19:00:07 +03:00
s . traefikCmd ( withConfigFile ( file ) )
2021-11-25 13:10:06 +03:00
2024-01-09 19:00:07 +03:00
err := try . GetRequest ( "http://127.0.0.1:8080/api/rawdata" , 5 * time . Second , try . StatusCodeIs ( http . StatusOK ) , try . BodyContains ( "HostSNI(`whoami-b.test`)" ) )
require . NoError ( s . T ( ) , err )
2021-11-25 13:10:06 +03:00
call := map [ string ] int { }
2024-02-19 17:44:03 +03:00
for range 4 {
2021-11-25 13:10:06 +03:00
// Traefik passes through, termination handled by whoami-b or whoami-bb
out , err := guessWhoTLSPassthrough ( "127.0.0.1:8093" , "whoami-b.test" )
2024-01-09 19:00:07 +03:00
require . NoError ( s . T ( ) , err )
2021-11-25 13:10:06 +03:00
switch {
case strings . Contains ( out , "whoami-b" ) :
call [ "whoami-b" ] ++
case strings . Contains ( out , "whoami-ab" ) :
call [ "whoami-ab" ] ++
default :
call [ "unknown" ] ++
}
time . Sleep ( time . Second )
}
2024-02-07 19:14:07 +03:00
assert . EqualValues ( s . T ( ) , map [ string ] int { "whoami-b" : 3 , "whoami-ab" : 1 } , call )
2021-11-25 13:10:06 +03:00
}
2019-06-07 20:30:07 +03:00
func welcome ( addr string ) ( string , error ) {
tcpAddr , err := net . ResolveTCPAddr ( "tcp" , addr )
if err != nil {
return "" , err
}
conn , err := net . DialTCP ( "tcp" , nil , tcpAddr )
if err != nil {
return "" , err
}
defer conn . Close ( )
out := make ( [ ] byte , 2048 )
n , err := conn . Read ( out )
if err != nil {
return "" , err
}
return string ( out [ : n ] ) , nil
}
2019-03-14 11:30:04 +03:00
func guessWho ( addr , serverName string , tlsCall bool ) ( string , error ) {
2019-06-17 19:14:08 +03:00
return guessWhoTLSMaxVersion ( addr , serverName , tlsCall , 0 )
}
func guessWhoTLSMaxVersion ( addr , serverName string , tlsCall bool , tlsMaxVersion uint16 ) ( string , error ) {
2019-03-14 11:30:04 +03:00
var conn net . Conn
var err error
if tlsCall {
2019-06-17 19:14:08 +03:00
conn , err = tls . Dial ( "tcp" , addr , & tls . Config {
ServerName : serverName ,
InsecureSkipVerify : true ,
MinVersion : 0 ,
MaxVersion : tlsMaxVersion ,
} )
2019-03-14 11:30:04 +03:00
} else {
tcpAddr , err2 := net . ResolveTCPAddr ( "tcp" , addr )
if err2 != nil {
return "" , err2
}
conn , err = net . DialTCP ( "tcp" , nil , tcpAddr )
if err != nil {
return "" , err
}
}
if err != nil {
return "" , err
}
defer conn . Close ( )
_ , err = conn . Write ( [ ] byte ( "WHO" ) )
if err != nil {
return "" , err
}
out := make ( [ ] byte , 2048 )
n , err := conn . Read ( out )
if err != nil {
return "" , err
}
return string ( out [ : n ] ) , nil
}
2019-09-13 21:00:06 +03:00
2021-11-25 13:10:06 +03:00
// guessWhoTLSPassthrough guesses service identity and ensures that the
// certificate is valid for the given server name.
func guessWhoTLSPassthrough ( addr , serverName string ) ( string , error ) {
var conn net . Conn
var err error
2019-09-13 21:00:06 +03:00
2021-11-25 13:10:06 +03:00
conn , err = tls . Dial ( "tcp" , addr , & tls . Config {
ServerName : serverName ,
InsecureSkipVerify : true ,
MinVersion : 0 ,
MaxVersion : 0 ,
VerifyPeerCertificate : func ( rawCerts [ ] [ ] byte , verifiedChains [ ] [ ] * x509 . Certificate ) error {
if len ( rawCerts ) > 1 {
return errors . New ( "tls: more than one certificates from peer" )
}
cert , err := x509 . ParseCertificate ( rawCerts [ 0 ] )
if err != nil {
return fmt . Errorf ( "tls: failed to parse certificate from peer: %w" , err )
}
if cert . Subject . CommonName == serverName {
return nil
}
if err = cert . VerifyHostname ( serverName ) ; err == nil {
return nil
}
return fmt . Errorf ( "tls: no valid certificate for serverName %s" , serverName )
} ,
} )
if err != nil {
return "" , err
}
defer conn . Close ( )
2019-09-13 21:00:06 +03:00
2021-11-25 13:10:06 +03:00
_ , err = conn . Write ( [ ] byte ( "WHO" ) )
if err != nil {
return "" , err
}
2019-09-13 21:00:06 +03:00
2021-11-25 13:10:06 +03:00
out := make ( [ ] byte , 2048 )
n , err := conn . Read ( out )
if err != nil {
return "" , err
2019-09-13 21:00:06 +03:00
}
2021-11-25 13:10:06 +03:00
return string ( out [ : n ] ) , nil
2019-09-13 21:00:06 +03:00
}