add proxy auth feature
This commit is contained in:
parent
8e568b6451
commit
6b8d16042e
114
client.go
114
client.go
@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ginuerzh/gosocks5"
|
"github.com/ginuerzh/gosocks5"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
@ -58,10 +59,18 @@ func listenAndServe(addr string, handler func(net.Conn)) error {
|
|||||||
|
|
||||||
func clientMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
func clientMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
||||||
switch method {
|
switch method {
|
||||||
|
case gosocks5.MethodUserPass:
|
||||||
|
user, pass := parseUserPass(Password)
|
||||||
|
if err := clientSocksAuth(conn, user, pass); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case MethodTLS, MethodTLSAuth:
|
case MethodTLS, MethodTLSAuth:
|
||||||
conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
conn = tls.Client(conn, &tls.Config{InsecureSkipVerify: true})
|
||||||
if method == MethodTLSAuth {
|
if method == MethodTLSAuth {
|
||||||
if err := cliTLSAuth(conn); err != nil {
|
if len(Password) == 0 {
|
||||||
|
return nil, ErrEmptyAuth
|
||||||
|
}
|
||||||
|
if err := clientSocksAuth(conn, "", Password); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,26 +89,6 @@ func clientMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cliTLSAuth(conn net.Conn) error {
|
|
||||||
if len(Password) == 0 {
|
|
||||||
return ErrEmptyPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gosocks5.NewUserPassRequest(
|
|
||||||
gosocks5.UserPassVer, "", Password).Write(conn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res, err := gosocks5.ReadUserPassResponse(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if res.Status != gosocks5.Succeeded {
|
|
||||||
return gosocks5.ErrAuthFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeTunnel() (c net.Conn, err error) {
|
func makeTunnel() (c net.Conn, err error) {
|
||||||
if UseWebsocket || !UseHttp {
|
if UseWebsocket || !UseHttp {
|
||||||
c, err = connect(Saddr)
|
c, err = connect(Saddr)
|
||||||
@ -161,21 +150,19 @@ func cliHandle(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if b[0] == gosocks5.Ver5 {
|
if b[0] == gosocks5.Ver5 {
|
||||||
length := 2 + int(b[1])
|
mn := int(b[1]) // methods count
|
||||||
|
length := 2 + mn
|
||||||
if n < length {
|
if n < length {
|
||||||
if _, err := io.ReadFull(conn, b[n:length]); err != nil {
|
if _, err := io.ReadFull(conn, b[n:length]); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gosocks5.WriteMethod(gosocks5.MethodNoAuth, conn); err != nil {
|
methods := b[2 : 2+mn]
|
||||||
return
|
handleSocks5(conn, methods)
|
||||||
}
|
|
||||||
|
|
||||||
handleSocks5(conn)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println(string(b[:n]))
|
||||||
for {
|
for {
|
||||||
if bytes.HasSuffix(b[:n], []byte("\r\n\r\n")) {
|
if bytes.HasSuffix(b[:n], []byte("\r\n\r\n")) {
|
||||||
break
|
break
|
||||||
@ -197,7 +184,51 @@ func cliHandle(conn net.Conn) {
|
|||||||
handleHttp(req, conn)
|
handleHttp(req, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSocks5(conn net.Conn) {
|
func selectMethod(conn net.Conn, methods ...uint8) error {
|
||||||
|
m := gosocks5.MethodNoAuth
|
||||||
|
|
||||||
|
if listenUrl.User != nil {
|
||||||
|
for _, method := range methods {
|
||||||
|
if method == gosocks5.MethodUserPass {
|
||||||
|
m = method
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if m != gosocks5.MethodUserPass {
|
||||||
|
m = gosocks5.MethodNoAcceptable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := gosocks5.WriteMethod(m, conn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(m)
|
||||||
|
|
||||||
|
switch m {
|
||||||
|
case gosocks5.MethodUserPass:
|
||||||
|
var username, password string
|
||||||
|
|
||||||
|
if listenUrl != nil && listenUrl.User != nil {
|
||||||
|
username = listenUrl.User.Username()
|
||||||
|
password, _ = listenUrl.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serverSocksAuth(conn, username, password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case gosocks5.MethodNoAcceptable:
|
||||||
|
return gosocks5.ErrBadMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleSocks5(conn net.Conn, methods []uint8) {
|
||||||
|
if err := selectMethod(conn, methods...); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
req, err := gosocks5.ReadRequest(conn)
|
req, err := gosocks5.ReadRequest(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -301,10 +332,35 @@ func cliTunnelUDP(uconn *net.UDPConn, sconn net.Conn) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientHttpAuth(req *http.Request, conn net.Conn, username, password string) error {
|
||||||
|
u, p, ok := req.BasicAuth()
|
||||||
|
if !ok ||
|
||||||
|
(len(username) > 0 && u != username) ||
|
||||||
|
(len(password) > 0 && p != password) {
|
||||||
|
conn.Write([]byte("HTTP/1.1 401 Not Authorized\r\n" +
|
||||||
|
"WWW-Authenticate: Basic realm=\"Authorization Required\"\r\n" +
|
||||||
|
"Proxy-Agent: gost/" + Version + "\r\n\r\n"))
|
||||||
|
|
||||||
|
return errors.New("Not Authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func handleHttp(req *http.Request, conn net.Conn) {
|
func handleHttp(req *http.Request, conn net.Conn) {
|
||||||
var host string
|
var host string
|
||||||
var port uint16
|
var port uint16
|
||||||
|
|
||||||
|
if listenUrl != nil && listenUrl.User != nil {
|
||||||
|
username := listenUrl.User.Username()
|
||||||
|
password, _ := listenUrl.User.Password()
|
||||||
|
|
||||||
|
if err := clientHttpAuth(req, conn, username, password); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s := strings.Split(req.Host, ":")
|
s := strings.Split(req.Host, ":")
|
||||||
host = s[0]
|
host = s[0]
|
||||||
port = 80
|
port = 80
|
||||||
|
15
main.go
15
main.go
@ -3,7 +3,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
//"github.com/ginuerzh/gosocks5"
|
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@ -18,7 +17,8 @@ var (
|
|||||||
CertFile, KeyFile string
|
CertFile, KeyFile string
|
||||||
PrintVersion bool
|
PrintVersion bool
|
||||||
|
|
||||||
proxyURL *url.URL
|
proxyURL *url.URL
|
||||||
|
listenUrl *url.URL
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -40,6 +40,7 @@ func init() {
|
|||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||||
|
|
||||||
proxyURL, _ = parseURL(Proxy)
|
proxyURL, _ = parseURL(Proxy)
|
||||||
|
listenUrl, _ = parseURL(Laddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -54,18 +55,20 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
laddr := listenUrl.Host
|
||||||
|
|
||||||
if len(Saddr) == 0 {
|
if len(Saddr) == 0 {
|
||||||
var server Server
|
var server Server
|
||||||
if UseWebsocket {
|
if UseWebsocket {
|
||||||
server = &WSServer{Addr: Laddr}
|
server = &WSServer{Addr: laddr}
|
||||||
} else if UseHttp {
|
} else if UseHttp {
|
||||||
server = &HttpServer{Addr: Laddr}
|
server = &HttpServer{Addr: laddr}
|
||||||
} else {
|
} else {
|
||||||
server = &Socks5Server{Addr: Laddr}
|
server = &Socks5Server{Addr: laddr}
|
||||||
}
|
}
|
||||||
log.Fatal(server.ListenAndServe())
|
log.Fatal(server.ListenAndServe())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(listenAndServe(Laddr, cliHandle))
|
log.Fatal(listenAndServe(laddr, cliHandle))
|
||||||
}
|
}
|
||||||
|
50
socks5.go
50
socks5.go
@ -94,6 +94,7 @@ func (s *Socks5Server) ListenAndServe() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serverSelectMethod(methods ...uint8) uint8 {
|
func serverSelectMethod(methods ...uint8) uint8 {
|
||||||
|
log.Println(methods)
|
||||||
m := gosocks5.MethodNoAuth
|
m := gosocks5.MethodNoAuth
|
||||||
|
|
||||||
for _, method := range methods {
|
for _, method := range methods {
|
||||||
@ -102,6 +103,11 @@ func serverSelectMethod(methods ...uint8) uint8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when user/pass is set for proxy auth, the NoAuth method is disabled
|
||||||
|
if len(Method) == 0 && m == gosocks5.MethodNoAuth && listenUrl.User != nil {
|
||||||
|
return gosocks5.MethodNoAcceptable
|
||||||
|
}
|
||||||
|
|
||||||
if len(Method) == 0 || Methods[m] == Method {
|
if len(Method) == 0 || Methods[m] == Method {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@ -110,7 +116,19 @@ func serverSelectMethod(methods ...uint8) uint8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serverMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
func serverMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
||||||
|
log.Println(method)
|
||||||
switch method {
|
switch method {
|
||||||
|
case gosocks5.MethodUserPass:
|
||||||
|
var username, password string
|
||||||
|
|
||||||
|
if listenUrl != nil && listenUrl.User != nil {
|
||||||
|
username = listenUrl.User.Username()
|
||||||
|
password, _ = listenUrl.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serverSocksAuth(conn, username, password); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case MethodTLS, MethodTLSAuth:
|
case MethodTLS, MethodTLSAuth:
|
||||||
var cert tls.Certificate
|
var cert tls.Certificate
|
||||||
var err error
|
var err error
|
||||||
@ -126,7 +144,11 @@ func serverMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
conn = tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
|
conn = tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||||
if method == MethodTLSAuth {
|
if method == MethodTLSAuth {
|
||||||
if err := svrTLSAuth(conn); err != nil {
|
// password is mandatory
|
||||||
|
if len(Password) == 0 {
|
||||||
|
return nil, ErrEmptyAuth
|
||||||
|
}
|
||||||
|
if err := serverSocksAuth(conn, "", Password); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,32 +166,6 @@ func serverMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func svrTLSAuth(conn net.Conn) error {
|
|
||||||
if len(Password) == 0 {
|
|
||||||
return ErrEmptyPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := gosocks5.ReadUserPassRequest(conn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.Password != Password {
|
|
||||||
if err := gosocks5.NewUserPassResponse(
|
|
||||||
gosocks5.UserPassVer, gosocks5.Failure).Write(conn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return gosocks5.ErrAuthFailure
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gosocks5.NewUserPassResponse(
|
|
||||||
gosocks5.UserPassVer, gosocks5.Succeeded).Write(conn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveSocks5(conn net.Conn) {
|
func serveSocks5(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
100
util.go
100
util.go
@ -29,21 +29,22 @@ const (
|
|||||||
MethodTLSAuth
|
MethodTLSAuth
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrEmptyPassword = errors.New("empty key")
|
var ErrEmptyAuth = errors.New("empty auth")
|
||||||
|
|
||||||
var Methods = map[uint8]string{
|
var Methods = map[uint8]string{
|
||||||
gosocks5.MethodNoAuth: "", // 0x00
|
//gosocks5.MethodNoAuth: "", // 0x00
|
||||||
MethodTLS: "tls", // 0x80
|
gosocks5.MethodUserPass: "userpass", // 0x02
|
||||||
MethodAES128: "aes-128-cfb", // 0x81
|
MethodTLS: "tls", // 0x80
|
||||||
MethodAES192: "aes-192-cfb", // 0x82
|
MethodAES128: "aes-128-cfb", // 0x81
|
||||||
MethodAES256: "aes-256-cfb", // 0x83
|
MethodAES192: "aes-192-cfb", // 0x82
|
||||||
MethodDES: "des-cfb", // 0x84
|
MethodAES256: "aes-256-cfb", // 0x83
|
||||||
MethodBF: "bf-cfb", // 0x85
|
MethodDES: "des-cfb", // 0x84
|
||||||
MethodCAST5: "cast5-cfb", // 0x86
|
MethodBF: "bf-cfb", // 0x85
|
||||||
MethodRC4MD5: "rc4-md5", // 8x87
|
MethodCAST5: "cast5-cfb", // 0x86
|
||||||
MethodRC4: "rc4", // 0x88
|
MethodRC4MD5: "rc4-md5", // 8x87
|
||||||
MethodTable: "table", // 0x89
|
MethodRC4: "rc4", // 0x88
|
||||||
MethodTLSAuth: "tls-auth", // 0x90
|
MethodTable: "table", // 0x89
|
||||||
|
MethodTLSAuth: "tls-auth", // 0x90
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseURL(rawurl string) (*url.URL, error) {
|
func parseURL(rawurl string) (*url.URL, error) {
|
||||||
@ -57,6 +58,15 @@ func parseURL(rawurl string) (*url.URL, error) {
|
|||||||
return url.Parse(rawurl)
|
return url.Parse(rawurl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseUserPass(key string) (username string, password string) {
|
||||||
|
sep := ":"
|
||||||
|
i := strings.Index(key, sep)
|
||||||
|
if i < 0 {
|
||||||
|
return key, ""
|
||||||
|
}
|
||||||
|
return key[0:i], key[i+len(sep):]
|
||||||
|
}
|
||||||
|
|
||||||
func ToSocksAddr(addr net.Addr) *gosocks5.Addr {
|
func ToSocksAddr(addr net.Addr) *gosocks5.Addr {
|
||||||
host, port, _ := net.SplitHostPort(addr.String())
|
host, port, _ := net.SplitHostPort(addr.String())
|
||||||
p, _ := strconv.Atoi(port)
|
p, _ := strconv.Atoi(port)
|
||||||
@ -135,9 +145,12 @@ func connectSocks5Proxy(addr string) (conn net.Conn, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conf := &gosocks5.Config{
|
conf := &gosocks5.Config{
|
||||||
Methods: []uint8{gosocks5.MethodNoAuth, gosocks5.MethodUserPass},
|
// Methods: []uint8{gosocks5.MethodNoAuth, gosocks5.MethodUserPass},
|
||||||
MethodSelected: proxyMethodSelected,
|
MethodSelected: proxyMethodSelected,
|
||||||
}
|
}
|
||||||
|
if proxyURL.User != nil {
|
||||||
|
conf.Methods = []uint8{gosocks5.MethodUserPass}
|
||||||
|
}
|
||||||
|
|
||||||
c := gosocks5.ClientConn(conn, conf)
|
c := gosocks5.ClientConn(conn, conf)
|
||||||
if err := c.Handleshake(); err != nil {
|
if err := c.Handleshake(); err != nil {
|
||||||
@ -178,30 +191,61 @@ func connectSocks5Proxy(addr string) (conn net.Conn, err error) {
|
|||||||
func proxyMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
func proxyMethodSelected(method uint8, conn net.Conn) (net.Conn, error) {
|
||||||
switch method {
|
switch method {
|
||||||
case gosocks5.MethodUserPass:
|
case gosocks5.MethodUserPass:
|
||||||
if proxyURL == nil || proxyURL.User == nil {
|
var user, pass string
|
||||||
return nil, gosocks5.ErrAuthFailure
|
|
||||||
|
if proxyURL != nil && proxyURL.User != nil {
|
||||||
|
user = proxyURL.User.Username()
|
||||||
|
pass, _ = proxyURL.User.Password()
|
||||||
}
|
}
|
||||||
pwd, _ := proxyURL.User.Password()
|
if err := clientSocksAuth(conn, user, pass); err != nil {
|
||||||
if err := gosocks5.NewUserPassRequest(gosocks5.UserPassVer,
|
|
||||||
proxyURL.User.Username(), pwd).Write(conn); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
resp, err := gosocks5.ReadUserPassResponse(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if resp.Status != gosocks5.Succeeded {
|
|
||||||
return nil, gosocks5.ErrAuthFailure
|
|
||||||
}
|
|
||||||
case gosocks5.MethodNoAcceptable:
|
case gosocks5.MethodNoAcceptable:
|
||||||
return nil, gosocks5.ErrBadMethod
|
return nil, gosocks5.ErrBadMethod
|
||||||
|
|
||||||
//case gosocks5.MethodNoAuth:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientSocksAuth(conn net.Conn, username, password string) error {
|
||||||
|
if err := gosocks5.NewUserPassRequest(
|
||||||
|
gosocks5.UserPassVer, username, password).Write(conn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
res, err := gosocks5.ReadUserPassResponse(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if res.Status != gosocks5.Succeeded {
|
||||||
|
return gosocks5.ErrAuthFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serverSocksAuth(conn net.Conn, username, password string) error {
|
||||||
|
req, err := gosocks5.ReadUserPassRequest(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len(username) > 0 && req.Username != username) ||
|
||||||
|
(len(password) > 0 && req.Password != password) {
|
||||||
|
if err := gosocks5.NewUserPassResponse(
|
||||||
|
gosocks5.UserPassVer, gosocks5.Failure).Write(conn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gosocks5.ErrAuthFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gosocks5.NewUserPassResponse(
|
||||||
|
gosocks5.UserPassVer, gosocks5.Succeeded).Write(conn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func setBasicAuth(r *http.Request) {
|
func setBasicAuth(r *http.Request) {
|
||||||
if proxyURL != nil && proxyURL.User != nil {
|
if proxyURL != nil && proxyURL.User != nil {
|
||||||
r.Header.Set("Proxy-Authorization",
|
r.Header.Set("Proxy-Authorization",
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Version = "1.6"
|
Version = "1.7"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
|
Loading…
Reference in New Issue
Block a user