creds: Add new NetrcCredentialHelper
This commit adds a new credential helper, NetrcCredentialHelper, to retrieve credentials from a .netrc file. This replaces the old .netrc authorization behaviour, which was done directly from the `doWithAuth()` code. Additionally, this commit moves all of the `.netrc` functionality out of `lfsapi` and into `creds`. This was done both because `creds` is now the logical place for the `.netrc` functionality, and to prevent an import cycle between `creds` and `lfsapi`.
This commit is contained in:
parent
e7b6b33b5a
commit
1026ff073c
@ -41,6 +41,7 @@ func bufferCreds(c Creds) *bytes.Buffer {
|
||||
}
|
||||
|
||||
type CredentialHelperContext struct {
|
||||
netrcCredHelper *netrcCredentialHelper
|
||||
commandCredHelper *commandCredentialHelper
|
||||
askpassCredHelper *AskPassCredentialHelper
|
||||
cachingCredHelper *credentialCacher
|
||||
@ -51,6 +52,8 @@ type CredentialHelperContext struct {
|
||||
func NewCredentialHelperContext(gitEnv config.Environment, osEnv config.Environment) *CredentialHelperContext {
|
||||
c := &CredentialHelperContext{urlConfig: config.NewURLConfig(gitEnv)}
|
||||
|
||||
c.netrcCredHelper = newNetrcCredentialHelper(osEnv)
|
||||
|
||||
askpass, ok := osEnv.Get("GIT_ASKPASS")
|
||||
if !ok {
|
||||
askpass, ok = gitEnv.Get("core.askpass")
|
||||
@ -95,7 +98,10 @@ func (ctxt *CredentialHelperContext) GetCredentialHelper(helper CredentialHelper
|
||||
return helper, input
|
||||
}
|
||||
|
||||
helpers := make([]CredentialHelper, 0, 3)
|
||||
helpers := make([]CredentialHelper, 0, 4)
|
||||
if ctxt.netrcCredHelper != nil {
|
||||
helpers = append(helpers, ctxt.netrcCredHelper)
|
||||
}
|
||||
if ctxt.cachingCredHelper != nil {
|
||||
helpers = append(helpers, ctxt.cachingCredHelper)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package lfsapi
|
||||
package creds
|
||||
|
||||
var netrcBasename = ".netrc"
|
@ -1,5 +1,5 @@
|
||||
// +build windows
|
||||
|
||||
package lfsapi
|
||||
package creds
|
||||
|
||||
var netrcBasename = "_netrc"
|
130
creds/netrc.go
Normal file
130
creds/netrc.go
Normal file
@ -0,0 +1,130 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/go-netrc/netrc"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
type NetrcFinder interface {
|
||||
FindMachine(string) *netrc.Machine
|
||||
}
|
||||
|
||||
func ParseNetrc(osEnv config.Environment) (NetrcFinder, string, error) {
|
||||
home, _ := osEnv.Get("HOME")
|
||||
if len(home) == 0 {
|
||||
return &noFinder{}, "", nil
|
||||
}
|
||||
|
||||
nrcfilename := filepath.Join(home, netrcBasename)
|
||||
if _, err := os.Stat(nrcfilename); err != nil {
|
||||
return &noFinder{}, nrcfilename, nil
|
||||
}
|
||||
|
||||
f, err := netrc.ParseFile(nrcfilename)
|
||||
return f, nrcfilename, err
|
||||
}
|
||||
|
||||
type noFinder struct{}
|
||||
|
||||
func (f *noFinder) FindMachine(host string) *netrc.Machine {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetrcCredentialHelper retrieves credentials from a .netrc file
|
||||
type netrcCredentialHelper struct {
|
||||
netrcFinder NetrcFinder
|
||||
skip map[string]bool
|
||||
}
|
||||
|
||||
var defaultNetrcFinder = &noFinder{}
|
||||
|
||||
// NewNetrcCredentialHelper creates a new netrc credential helper using a
|
||||
// .netrc file gleaned from the OS environment
|
||||
func newNetrcCredentialHelper(osEnv config.Environment) *netrcCredentialHelper {
|
||||
netrcFinder, netrcfile, err := ParseNetrc(osEnv)
|
||||
if err != nil {
|
||||
tracerx.Printf("bad netrc file %s: %s", netrcfile, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
return &netrcCredentialHelper{netrcFinder: netrcFinder, skip: make(map[string]bool)}
|
||||
}
|
||||
|
||||
func (c *netrcCredentialHelper) Fill(what Creds) (Creds, error) {
|
||||
host, err := getNetrcHostname(what["host"])
|
||||
if err != nil {
|
||||
return nil, credHelperNoOp
|
||||
}
|
||||
|
||||
if c.skip[host] {
|
||||
return nil, credHelperNoOp
|
||||
}
|
||||
|
||||
if machine := c.netrcFinder.FindMachine(host); machine != nil {
|
||||
creds := make(Creds)
|
||||
creds["username"] = machine.Login
|
||||
creds["password"] = machine.Password
|
||||
creds["protocol"] = what["protocol"]
|
||||
creds["host"] = what["host"]
|
||||
creds["scheme"] = what["scheme"]
|
||||
creds["path"] = what["path"]
|
||||
creds["source"] = "netrc"
|
||||
tracerx.Printf("netrc: git credential fill (%q, %q, %q)",
|
||||
what["protocol"], what["host"], what["path"])
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
return nil, credHelperNoOp
|
||||
}
|
||||
|
||||
func getNetrcHostname(hostname string) (string, error) {
|
||||
if strings.Contains(hostname, ":") {
|
||||
host, _, err := net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
|
||||
return "", err
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
func (c *netrcCredentialHelper) Approve(what Creds) error {
|
||||
if what["source"] == "netrc" {
|
||||
host, err := getNetrcHostname(what["host"])
|
||||
if err != nil {
|
||||
return credHelperNoOp
|
||||
}
|
||||
tracerx.Printf("netrc: git credential approve (%q, %q, %q)",
|
||||
what["protocol"], what["host"], what["path"])
|
||||
c.skip[host] = false
|
||||
return nil
|
||||
}
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
func (c *netrcCredentialHelper) Reject(what Creds) error {
|
||||
if what["source"] == "netrc" {
|
||||
host, err := getNetrcHostname(what["host"])
|
||||
if err != nil {
|
||||
return credHelperNoOp
|
||||
}
|
||||
|
||||
tracerx.Printf("netrc: git credential reject (%q, %q, %q)",
|
||||
what["protocol"], what["host"], what["path"])
|
||||
c.skip[host] = true
|
||||
return nil
|
||||
}
|
||||
return credHelperNoOp
|
||||
}
|
82
creds/netrc_test.go
Normal file
82
creds/netrc_test.go
Normal file
@ -0,0 +1,82 @@
|
||||
package creds
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/go-netrc/netrc"
|
||||
)
|
||||
|
||||
func TestNetrcWithHostAndPort(t *testing.T) {
|
||||
var netrcHelper netrcCredentialHelper
|
||||
netrcHelper.netrcFinder = &fakeNetrc{}
|
||||
|
||||
what := make(Creds)
|
||||
what["protocol"] = "http"
|
||||
what["host"] = "netrc-host:123"
|
||||
what["path"] = "/foo/bar"
|
||||
|
||||
creds, err := netrcHelper.Fill(what)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving netrc credentials: %s", err)
|
||||
}
|
||||
|
||||
username := creds["username"]
|
||||
if username != "abc" {
|
||||
t.Fatalf("bad username: %s", username)
|
||||
}
|
||||
|
||||
password := creds["password"]
|
||||
if password != "def" {
|
||||
t.Fatalf("bad password: %s", password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithHost(t *testing.T) {
|
||||
var netrcHelper netrcCredentialHelper
|
||||
netrcHelper.netrcFinder = &fakeNetrc{}
|
||||
|
||||
what := make(Creds)
|
||||
what["protocol"] = "http"
|
||||
what["host"] = "netrc-host"
|
||||
what["path"] = "/foo/bar"
|
||||
|
||||
creds, err := netrcHelper.Fill(what)
|
||||
if err != nil {
|
||||
t.Fatalf("error retrieving netrc credentials: %s", err)
|
||||
}
|
||||
|
||||
username := creds["username"]
|
||||
if username != "abc" {
|
||||
t.Fatalf("bad username: %s", username)
|
||||
}
|
||||
|
||||
password := creds["password"]
|
||||
if password != "def" {
|
||||
t.Fatalf("bad password: %s", password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithBadHost(t *testing.T) {
|
||||
var netrcHelper netrcCredentialHelper
|
||||
netrcHelper.netrcFinder = &fakeNetrc{}
|
||||
|
||||
what := make(Creds)
|
||||
what["protocol"] = "http"
|
||||
what["host"] = "other-host"
|
||||
what["path"] = "/foo/bar"
|
||||
|
||||
_, err := netrcHelper.Fill(what)
|
||||
if err != credHelperNoOp {
|
||||
t.Fatalf("expected no-op for unknown host other-host")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeNetrc struct{}
|
||||
|
||||
func (n *fakeNetrc) FindMachine(host string) *netrc.Machine {
|
||||
if strings.Contains(host, "netrc") {
|
||||
return &netrc.Machine{Login: "abc", Password: "def"}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -3,7 +3,6 @@ package lfsapi
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@ -12,12 +11,10 @@ import (
|
||||
"github.com/git-lfs/git-lfs/creds"
|
||||
"github.com/git-lfs/git-lfs/errors"
|
||||
"github.com/git-lfs/git-lfs/lfshttp"
|
||||
"github.com/git-lfs/go-netrc/netrc"
|
||||
"github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultNetrcFinder = &noFinder{}
|
||||
defaultEndpointFinder = NewEndpointFinder(nil)
|
||||
)
|
||||
|
||||
@ -134,16 +131,11 @@ func (c *Client) getCreds(remote string, access Access, req *http.Request) (cred
|
||||
ef = defaultEndpointFinder
|
||||
}
|
||||
|
||||
netrcFinder := c.Netrc
|
||||
if netrcFinder == nil {
|
||||
netrcFinder = defaultNetrcFinder
|
||||
}
|
||||
|
||||
operation := getReqOperation(req)
|
||||
apiEndpoint := ef.Endpoint(operation, remote)
|
||||
|
||||
if access.Mode() != NTLMAccess {
|
||||
if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access.Mode() == NoneAccess {
|
||||
if requestHasAuth(req) || access.Mode() == NoneAccess {
|
||||
return creds.NullCreds, nil, nil, nil
|
||||
}
|
||||
|
||||
@ -171,18 +163,6 @@ func (c *Client) getCreds(remote string, access Access, req *http.Request) (cred
|
||||
return creds.NullCreds, nil, nil, errors.Wrap(err, "creds")
|
||||
}
|
||||
|
||||
if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
|
||||
cred := creds.Creds{
|
||||
"protocol": credsURL.Scheme,
|
||||
"host": credsURL.Host,
|
||||
"username": netrcMachine.Login,
|
||||
"password": netrcMachine.Password,
|
||||
"source": "netrc",
|
||||
}
|
||||
|
||||
return creds.NullCreds, credsURL, cred, nil
|
||||
}
|
||||
|
||||
// NTLM uses creds to create the session
|
||||
credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
|
||||
return credHelper, credsURL, creds, err
|
||||
@ -204,33 +184,6 @@ func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (
|
||||
return credHelper, creds, err
|
||||
}
|
||||
|
||||
func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
|
||||
hostname := req.URL.Host
|
||||
var host string
|
||||
|
||||
if strings.Contains(hostname, ":") {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(hostname)
|
||||
if err != nil {
|
||||
tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
host = hostname
|
||||
}
|
||||
|
||||
return netrcFinder.FindMachine(host)
|
||||
}
|
||||
|
||||
func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool {
|
||||
if machine := getAuthFromNetrc(netrcFinder, req); machine != nil {
|
||||
setRequestAuth(req, machine.Login, machine.Password)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint lfshttp.Endpoint, req *http.Request) (*url.URL, error) {
|
||||
apiURL, err := url.Parse(apiEndpoint.Url)
|
||||
if err != nil {
|
||||
|
@ -448,27 +448,6 @@ func TestGetCreds(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
"ntlm with netrc": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://netrc-host.com/repo/lfs/locks",
|
||||
Endpoint: "https://netrc-host.com/repo/lfs",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://netrc-host.com/repo/lfs",
|
||||
"lfs.https://netrc-host.com/repo/lfs.access": "ntlm",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: NTLMAccess,
|
||||
CredsURL: "https://netrc-host.com/repo/lfs",
|
||||
Creds: map[string]string{
|
||||
"protocol": "https",
|
||||
"host": "netrc-host.com",
|
||||
"username": "abc",
|
||||
"password": "def",
|
||||
"source": "netrc",
|
||||
},
|
||||
},
|
||||
},
|
||||
"custom auth": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
@ -486,20 +465,6 @@ func TestGetCreds(t *testing.T) {
|
||||
Authorization: "custom",
|
||||
},
|
||||
},
|
||||
"netrc": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
Href: "https://netrc-host.com/repo/lfs/locks",
|
||||
Endpoint: "https://netrc-host.com/repo/lfs",
|
||||
Config: map[string]string{
|
||||
"lfs.url": "https://netrc-host.com/repo/lfs",
|
||||
"lfs.https://netrc-host.com/repo/lfs.access": "basic",
|
||||
},
|
||||
Expected: getCredsExpected{
|
||||
Access: BasicAccess,
|
||||
Authorization: basicAuth("abc", "def"),
|
||||
},
|
||||
},
|
||||
"username in url": getCredsTest{
|
||||
Remote: "origin",
|
||||
Method: "GET",
|
||||
@ -675,7 +640,6 @@ func TestGetCreds(t *testing.T) {
|
||||
ctx := lfshttp.NewContext(git.NewConfig("", ""), nil, test.Config)
|
||||
client, _ := NewClient(ctx)
|
||||
client.Credentials = &fakeCredentialFiller{}
|
||||
client.Netrc = &fakeNetrc{}
|
||||
client.Endpoints = NewEndpointFinder(ctx)
|
||||
_, credsURL, creds, err := client.getCreds(test.Remote, client.Endpoints.AccessFor(test.Endpoint), req)
|
||||
if !assert.Nil(t, err) {
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
type Client struct {
|
||||
Endpoints EndpointFinder
|
||||
Credentials creds.CredentialHelper
|
||||
Netrc NetrcFinder
|
||||
|
||||
ntlmSessions map[string]ntlm.ClientSession
|
||||
ntlmMu sync.Mutex
|
||||
@ -30,10 +29,6 @@ func NewClient(ctx lfshttp.Context) (*Client, error) {
|
||||
|
||||
gitEnv := ctx.GitEnv()
|
||||
osEnv := ctx.OSEnv()
|
||||
netrc, netrcfile, err := ParseNetrc(osEnv)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("bad netrc file %s", netrcfile))
|
||||
}
|
||||
|
||||
httpClient, err := lfshttp.NewClient(ctx)
|
||||
if err != nil {
|
||||
@ -42,7 +37,6 @@ func NewClient(ctx lfshttp.Context) (*Client, error) {
|
||||
|
||||
c := &Client{
|
||||
Endpoints: NewEndpointFinder(ctx),
|
||||
Netrc: netrc,
|
||||
client: httpClient,
|
||||
credContext: creds.NewCredentialHelperContext(gitEnv, osEnv),
|
||||
}
|
||||
|
@ -1,34 +0,0 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/git-lfs/git-lfs/config"
|
||||
"github.com/git-lfs/go-netrc/netrc"
|
||||
)
|
||||
|
||||
type NetrcFinder interface {
|
||||
FindMachine(string) *netrc.Machine
|
||||
}
|
||||
|
||||
func ParseNetrc(osEnv config.Environment) (NetrcFinder, string, error) {
|
||||
home, _ := osEnv.Get("HOME")
|
||||
if len(home) == 0 {
|
||||
return &noFinder{}, "", nil
|
||||
}
|
||||
|
||||
nrcfilename := filepath.Join(home, netrcBasename)
|
||||
if _, err := os.Stat(nrcfilename); err != nil {
|
||||
return &noFinder{}, nrcfilename, nil
|
||||
}
|
||||
|
||||
f, err := netrc.ParseFile(nrcfilename)
|
||||
return f, nrcfilename, err
|
||||
}
|
||||
|
||||
type noFinder struct{}
|
||||
|
||||
func (f *noFinder) FindMachine(host string) *netrc.Machine {
|
||||
return nil
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package lfsapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/git-lfs/go-netrc/netrc"
|
||||
)
|
||||
|
||||
func TestNetrcWithHostAndPort(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://netrc-host:123/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatal("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithHost(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://netrc-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if !setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatalf("no netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "Basic YWJjOmRlZg==" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNetrcWithBadHost(t *testing.T) {
|
||||
netrcFinder := &fakeNetrc{}
|
||||
u, err := url.Parse("http://other-host/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
URL: u,
|
||||
Header: http.Header{},
|
||||
}
|
||||
|
||||
if setAuthFromNetrc(netrcFinder, req) {
|
||||
t.Fatalf("unexpected netrc match")
|
||||
}
|
||||
|
||||
auth := req.Header.Get("Authorization")
|
||||
if auth != "" {
|
||||
t.Fatalf("bad basic auth: %q", auth)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeNetrc struct{}
|
||||
|
||||
func (n *fakeNetrc) FindMachine(host string) *netrc.Machine {
|
||||
if strings.Contains(host, "netrc") {
|
||||
return &netrc.Machine{Login: "abc", Password: "def"}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -255,8 +255,14 @@ begin_test "credentials from netrc"
|
||||
|
||||
GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log
|
||||
grep "Uploading LFS objects: 100% (1/1), 7 B" push.log
|
||||
echo "any git credential calls:"
|
||||
[ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ]
|
||||
echo "any netrc credential calls:"
|
||||
[ "4" -eq "$(cat push.log | grep "netrc: git credential" | wc -l)" ]
|
||||
|
||||
echo "any netrc credential fills:"
|
||||
[ "2" -eq "$(cat push.log | grep "netrc: git credential fill" | wc -l)" ]
|
||||
|
||||
echo "any netrc credential approvals:"
|
||||
[ "2" -eq "$(cat push.log | grep "netrc: git credential approve" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
@ -288,8 +294,14 @@ begin_test "credentials from netrc with unknown keyword"
|
||||
|
||||
GIT_TRACE=1 git lfs push netrc master 2>&1 | tee push.log
|
||||
grep "Uploading LFS objects: 100% (1/1), 7 B" push.log
|
||||
echo "any git credential calls:"
|
||||
[ "0" -eq "$(cat push.log | grep "git credential" | wc -l)" ]
|
||||
echo "any netrc credential calls:"
|
||||
[ "4" -eq "$(cat push.log | grep "netrc: git credential" | wc -l)" ]
|
||||
|
||||
echo "any netrc credential fills:"
|
||||
[ "2" -eq "$(cat push.log | grep "netrc: git credential fill" | wc -l)" ]
|
||||
|
||||
echo "any netrc credential approvals:"
|
||||
[ "2" -eq "$(cat push.log | grep "netrc: git credential approve" | wc -l)" ]
|
||||
)
|
||||
end_test
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user