docs: update docs and other fixes
This commit is contained in:
		
							parent
							
								
									5d111f0885
								
							
						
					
					
						commit
						09ab41650d
					
				@ -1 +0,0 @@
 | 
				
			|||||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCg0nrgmQO7Cxt34uuI9pqZq72nqY/CmcTf/ZAtyZydk3kS9JZBckk0iUcT+zWWDcImVw9GWrkAu58mzAMU97LrrVg7Ap+pAZl99UNOUNkC8HeJTN559l/eCZ6gohlAlaKV3qSWia7+aIwRabhjTPAd6oOuaQjifd+/uvfQUTHIIjzsM6g3fQz70IyilH7Iid0OXytNWLh0emRjE/qcJA0R27cGQXxiQEf8Pz6Tl1YFbp3qd7pk0scl8kobdKmNv6LSPUUNuSeJC3AMbrmiHp9XnVTUBh0PB8Yn6kqA0OCtNZ/29RI4boIyDI4Y+wJxZ65E45doEExRPyxMzeoEnaAacUieu97rzGRj0WvfBIKs8kGCPJ8JA9ZvFcWkQbZK2kIQ/AWk8utmpKewlUzdB3VBM2d5nrsr+zo7U92rD7GUy0SEpV7sNy9pmob8M9WB8Qnjtl7+VmC1w5yNnJCfrzXVo+k7RsL71m/EbbJZj35hd6ExhPLgSL+CFRl4sj6TqJrdFYH0LbhQqvTydc8R8N9MhaMxVdEkQ08M3KcjHgFh2jEDJGbd4cChcPt+jWcWKw34Ycf8R7WKPvB1v9FVYOxesBMCaElWLMdrQlVf118WjJ+A25ynCwCpzYOFfsRCb0JbH1e2ANOXV2HdXvTBQxtT6r1MUFVKWJPrfgWlAdA3mQ== jonathan.lundy@ip-10-240-248-37.ec2.internal
 | 
					 | 
				
			||||||
							
								
								
									
										30
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								README.md
									
									
									
									
									
								
							@ -1,6 +1,6 @@
 | 
				
			|||||||
# sshfwd
 | 
					# sshfwd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This is a reverse forward service that uses SSH as the transport. It works similar to ngrok or localtunnel.me.
 | 
					This is a reverse proxy service that uses SSH as the transport. It works similar to ngrok or localtunnel.me.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
You run the service on a internet addressible host and ssh to it. Using ssh remote forwards (ie. ssh -R) the port on the remote host will be forwared to
 | 
					You run the service on a internet addressible host and ssh to it. Using ssh remote forwards (ie. ssh -R) the port on the remote host will be forwared to
 | 
				
			||||||
the configured port on your local machine.
 | 
					the configured port on your local machine.
 | 
				
			||||||
@ -9,21 +9,31 @@ on Remote host:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
$ make genkeys  # generate the services host keys.
 | 
					$ make genkeys  # generate the services host keys.
 | 
				
			||||||
$ SSH_HOSTKEYS=hostkeys SSH_LISTEN=:2222 sshfwd   # run service on port 2222
 | 
					$ SSH_HOSTKEYS=hostkeys SSH_LISTEN=:2222 SSH_DOMAIN=example.com sshfwd   # run service on port 2222
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
on your local machine:
 | 
					For best results place this behind a TLS termination that has a wildcard certificate and CNAME for `*.yourdomain.com`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on your local machine have a ssh private and public key available:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
$ ssh -T remote.example.com -p 2222 -R 0.0.0.0:1234:localhost:3000
 | 
					$ export LOCAL_PORT=3000; export PRIV_KEY=~/.ssh/id_ed25519; sh -c "$(shell http --form POST example.com:2222 pub=@$(PRIV_KEY).pub)"
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
now if you access `remote.example.com:1234` it will be the same as accessing `localhost:3000`
 | 
					This will setup a reverse proxy on the example host that you can then use to access the local port. It will print a name unique to your ssh key.
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Pubkeys
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
if the env variable `SSH_AUTHKEYS` is set it will require that the client authenticates with one of the keys in the `SSH_AUTHKEYS` directory.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
```sh
 | 
					```sh
 | 
				
			||||||
$ SSH_LISTEN=:2222 SSH_HOSTKEYS=hostkeys SSH_AUTHKEYS=authkeys sshfwd
 | 
					$ http GET romeo-nine-lake.example.com:2222
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All accesses to the proxy will have the HTTP request printed out to the ssh connection.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					GET /connect HTTP/1.1
 | 
				
			||||||
 | 
					Host: romeo-nine-lake.example.com
 | 
				
			||||||
 | 
					Accept: */*
 | 
				
			||||||
 | 
					User-Agent: curl/7.64.1
 | 
				
			||||||
 | 
					X-Forwarded-Host: romeo-nine-lake.example.com
 | 
				
			||||||
 | 
					X-Origin-Host: [::1]:7000
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
				
			|||||||
@ -1 +0,0 @@
 | 
				
			|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKk9O/oAyP6OZetRFQUjjJeeQAZLqmlPCUwNvJzmAjwR jonlundy@MB-C02F1Q5XMD6M
 | 
					 | 
				
			||||||
@ -1 +0,0 @@
 | 
				
			|||||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIUSB+REbjTVPRKuHHCRaMJJlMIw6Ofp0oZvy7wDSZR4 jonathan.lundy@US17020047.local
 | 
					 | 
				
			||||||
							
								
								
									
										116
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										116
									
								
								main.go
									
									
									
									
									
								
							@ -25,11 +25,9 @@ import (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					const (
 | 
				
			||||||
	domainName          = "prox.int"
 | 
						domainName = "prox.int"
 | 
				
			||||||
	domainSuffix        = ".prox.int"
 | 
						portRange  = "7000-7999"
 | 
				
			||||||
	portStart    uint32 = 7000
 | 
						bindHost   = "[::1]"
 | 
				
			||||||
	portEnd      uint32 = 7999
 | 
					 | 
				
			||||||
	bindHost            = "[::1]"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var filterName = regexp.MustCompile("[^a-z0-9-]+")
 | 
					var filterName = regexp.MustCompile("[^a-z0-9-]+")
 | 
				
			||||||
@ -53,21 +51,19 @@ func run(ctx context.Context) {
 | 
				
			|||||||
		ssh.NoPty(),
 | 
							ssh.NoPty(),
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	files, err := ioutil.ReadDir(envMust("SSH_HOSTKEYS"))
 | 
						hostKeys := envMust("SSH_HOSTKEYS")
 | 
				
			||||||
 | 
						files, err := ioutil.ReadDir(hostKeys)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatal(err)
 | 
							log.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						for _, f := range files {
 | 
				
			||||||
 | 
							opts = append(opts, ssh.HostKeyFile(filepath.Join(hostKeys, f.Name())))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	srv := &server{
 | 
						srv := &server{
 | 
				
			||||||
		bindHost:     envDefault("SSH_HOST", bindHost),
 | 
							bindHost:     envDefault("SSH_HOST", bindHost),
 | 
				
			||||||
		portStart:    portStart,
 | 
					 | 
				
			||||||
		portEnd:      portEnd,
 | 
					 | 
				
			||||||
		domainName:   envDefault("SSH_DOMAIN", domainName),
 | 
							domainName:   envDefault("SSH_DOMAIN", domainName),
 | 
				
			||||||
		domainSuffix: envDefault("SSH_DOMAIN_SUFFIX", domainSuffix),
 | 
							domainSuffix: envDefault("SSH_DOMAIN_SUFFIX", "."+domainName),
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for _, f := range files {
 | 
					 | 
				
			||||||
		opts = append(opts, ssh.HostKeyFile(filepath.Join(envMust("SSH_HOSTKEYS"), f.Name())))
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	opts = append(opts, srv.optAuthUser()...)
 | 
						opts = append(opts, srv.optAuthUser()...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -81,7 +77,31 @@ func run(ctx context.Context) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log.Fatal(mux.Serve(ctx))
 | 
						if r := envDefault("SSH_PORTRANGE", portRange); r != "" {
 | 
				
			||||||
 | 
							sp := strings.SplitN(r, "-", 2)
 | 
				
			||||||
 | 
							if len(sp) == 1 {
 | 
				
			||||||
 | 
								log.Fatal("SSH_PORTRANGE should have start and end like 7000-7999")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var p uint64
 | 
				
			||||||
 | 
							if p, err = strconv.ParseUint(sp[0], 10, 32); err != nil {
 | 
				
			||||||
 | 
								log.Fatal("SSH_PORTRANGE start port invalid:", sp[0])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							srv.portStart = uint32(p)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if p, err = strconv.ParseUint(sp[1], 10, 32); err != nil {
 | 
				
			||||||
 | 
								log.Fatal("SSH_PORTRANGE end port invalid:", sp[1])
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							srv.portEnd = uint32(p)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if srv.portStart > srv.portEnd {
 | 
				
			||||||
 | 
								log.Fatalf("SSH_PORTRANGE is reversed %d > %d", srv.portStart, srv.portEnd)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err = mux.Serve(ctx); err != nil {
 | 
				
			||||||
 | 
							log.Fatal()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func envMust(s string) string {
 | 
					func envMust(s string) string {
 | 
				
			||||||
@ -95,7 +115,7 @@ func envMust(s string) string {
 | 
				
			|||||||
func envDefault(s, d string) string {
 | 
					func envDefault(s, d string) string {
 | 
				
			||||||
	v := os.Getenv(s)
 | 
						v := os.Getenv(s)
 | 
				
			||||||
	if v == "" {
 | 
						if v == "" {
 | 
				
			||||||
		return d
 | 
							v = d
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	log.Println("env", s, "==", v)
 | 
						log.Println("env", s, "==", v)
 | 
				
			||||||
	return v
 | 
						return v
 | 
				
			||||||
@ -110,7 +130,6 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
 | 
				
			|||||||
		if u, ok := srv.GetUserByName(s.User()); ok {
 | 
							if u, ok := srv.GetUserByName(s.User()); ok {
 | 
				
			||||||
			host := fmt.Sprintf("%v:%v", u.bindHost, u.bindPort)
 | 
								host := fmt.Sprintf("%v:%v", u.bindHost, u.bindPort)
 | 
				
			||||||
			director := func(req *http.Request) {
 | 
								director := func(req *http.Request) {
 | 
				
			||||||
				req = req.WithContext(s.Context())
 | 
					 | 
				
			||||||
				if h := req.Header.Get("X-Forwarded-Host"); h == "" {
 | 
									if h := req.Header.Get("X-Forwarded-Host"); h == "" {
 | 
				
			||||||
					req.Header.Set("X-Forwarded-Host", req.Host)
 | 
										req.Header.Set("X-Forwarded-Host", req.Host)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@ -120,12 +139,12 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
				requestDump, err := httputil.DumpRequest(req, req.Method == http.MethodPost || req.Method == http.MethodPut)
 | 
									requestDump, err := httputil.DumpRequest(req, req.Method == http.MethodPost || req.Method == http.MethodPut)
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
				  fmt.Println(err)
 | 
										fmt.Println(err)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				fmt.Fprintln(s, string(requestDump))
 | 
									fmt.Fprintln(s, string(requestDump))
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			u.proxy = &httputil.ReverseProxy{Director: director}
 | 
								u.proxy = &httputil.ReverseProxy{Director: director}
 | 
				
			||||||
			fmt.Fprintf(s, "Created HTTP listener at: %v%v\n", u.name, srv.domainSuffix)
 | 
								fmt.Fprintf(s, "Created HTTP listener at: %v%v\n\n", u.name, srv.domainSuffix)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		select {
 | 
							select {
 | 
				
			||||||
@ -136,6 +155,8 @@ func (srv *server) newSession(ctx context.Context) func(ssh.Session) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if u, ok := srv.GetUserByName(s.User()); ok {
 | 
							if u, ok := srv.GetUserByName(s.User()); ok {
 | 
				
			||||||
 | 
								u.ctx = nil
 | 
				
			||||||
 | 
								u.proxy = nil
 | 
				
			||||||
			srv.ports.Delete(u.bindPort)
 | 
								srv.ports.Delete(u.bindPort)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if _, err := fmt.Fprintf(s, "Goodbye! %s\n", s.User()); err != nil {
 | 
							if _, err := fmt.Fprintf(s, "Goodbye! %s\n", s.User()); err != nil {
 | 
				
			||||||
@ -158,18 +179,43 @@ type server struct {
 | 
				
			|||||||
	users sync.Map
 | 
						users sync.Map
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (s *server) String() string {
 | 
				
			||||||
 | 
						var b strings.Builder
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "Server:     ", s.domainName)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  Port:     ", s.listenPort)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  Suffix:   ", s.domainSuffix)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  BindHost: ", s.bindHost)
 | 
				
			||||||
 | 
						fmt.Fprintf(&b, "  PortRange: %d-%d\n", s.portStart, s.portEnd)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  NextPort: ", s.portNext)
 | 
				
			||||||
 | 
						return b.String()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type user struct {
 | 
					type user struct {
 | 
				
			||||||
	name     string
 | 
						name      string
 | 
				
			||||||
	pubkey   ssh.PublicKey
 | 
						pubkey    ssh.PublicKey
 | 
				
			||||||
	bindHost string
 | 
						bindHost  string
 | 
				
			||||||
	bindPort uint32
 | 
						bindPort  uint32
 | 
				
			||||||
	ctx      ssh.Context
 | 
						ctx       ssh.Context
 | 
				
			||||||
	proxy    http.Handler
 | 
						proxy     http.Handler
 | 
				
			||||||
 | 
						lastLogin time.Time
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *user) String() string {
 | 
				
			||||||
 | 
						var b strings.Builder
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "User:     ", u.name)
 | 
				
			||||||
 | 
						fmt.Fprintf(&b, "  Ptr:     %p\n", u)
 | 
				
			||||||
 | 
						fmt.Fprintf(&b, "  Pubkey:  %x\n", u.pubkey)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  Host:   ", u.bindHost)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  Port:   ", u.bindPort)
 | 
				
			||||||
 | 
						fmt.Fprintf(&b, "  Active:  %t\n", u.ctx != nil)
 | 
				
			||||||
 | 
						fmt.Fprintln(&b, "  LastLog:", u.lastLogin)
 | 
				
			||||||
 | 
						return b.String()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (srv *server) AddUser(pubkey ssh.PublicKey) *user {
 | 
					func (srv *server) AddUser(pubkey ssh.PublicKey) *user {
 | 
				
			||||||
	u := &user{}
 | 
						u := &user{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						u.lastLogin = time.Now()
 | 
				
			||||||
	u.name = fingerprintHuman(pubkey)
 | 
						u.name = fingerprintHuman(pubkey)
 | 
				
			||||||
	u.name = strings.ToLower(u.name)
 | 
						u.name = strings.ToLower(u.name)
 | 
				
			||||||
	u.name = filterName.ReplaceAllString(u.name, "")
 | 
						u.name = filterName.ReplaceAllString(u.name, "")
 | 
				
			||||||
@ -248,9 +294,13 @@ func (srv *server) optAuthUser() []ssh.Option {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if ssh.KeysEqual(key, u.pubkey) {
 | 
								if ssh.KeysEqual(key, u.pubkey) {
 | 
				
			||||||
				log.Println("User: ", ctx.User(), "Authorized:", u.bindHost, u.bindPort, ctx.ClientVersion(), ctx.SessionID(), ctx.LocalAddr(), ctx.RemoteAddr())
 | 
									log.Println("User:", ctx.User(), "Authorized:", u.bindHost, u.bindPort, ctx.ClientVersion(), ctx.SessionID(), ctx.LocalAddr(), ctx.RemoteAddr())
 | 
				
			||||||
				u.ctx = ctx
 | 
									u.ctx = ctx
 | 
				
			||||||
				srv.ports.Store(u.bindPort, u)
 | 
									u.lastLogin = time.Now()
 | 
				
			||||||
 | 
									if _, loaded := srv.ports.LoadOrStore(u.bindPort, u); loaded {
 | 
				
			||||||
 | 
										log.Println("User:", ctx.User(), "already connected!")
 | 
				
			||||||
 | 
										return false
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
				return true
 | 
									return true
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -306,7 +356,7 @@ func (srv *server) serveHTTP(ctx context.Context) func(net.Listener) error {
 | 
				
			|||||||
		ReadTimeout:  2500 * time.Millisecond,
 | 
							ReadTimeout:  2500 * time.Millisecond,
 | 
				
			||||||
		WriteTimeout: 5 * time.Second,
 | 
							WriteTimeout: 5 * time.Second,
 | 
				
			||||||
		Handler:      http.DefaultServeMux,
 | 
							Handler:      http.DefaultServeMux,
 | 
				
			||||||
		BaseContext: func(net.Listener) context.Context { return ctx },
 | 
							BaseContext:  func(net.Listener) context.Context { return ctx },
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	go func(ctx context.Context) {
 | 
						go func(ctx context.Context) {
 | 
				
			||||||
@ -338,16 +388,20 @@ func (srv *server) handleHTTP(rw http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		u := srv.AddUser(pubkey)
 | 
							u := srv.AddUser(pubkey)
 | 
				
			||||||
		rw.WriteHeader(201)
 | 
							rw.WriteHeader(201)
 | 
				
			||||||
		fmt.Fprintf(rw, `ssh -T -p %v %v@%v -R "%v:%v:localhost:$LOCAL_PORT" -i $PRIV_KEY`, srv.listenPort, u.name, srv.domainName, u.bindHost, u.bindPort)
 | 
							fmt.Fprintf(rw, `ssh -T -p %v %v@%v -R "%v:%v:localhost:$LOCAL_PORT" -i $PRIV_KEY`+"\n", srv.listenPort, u.name, srv.domainName, u.bindHost, u.bindPort)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	fmt.Fprintln(rw, "Hello!", r.Host, r.URL)
 | 
						fmt.Fprintln(rw, "Hello!")
 | 
				
			||||||
 | 
						fmt.Fprintln(rw, srv)
 | 
				
			||||||
 | 
						fmt.Fprintln(rw, "Registered Users")
 | 
				
			||||||
	for _, u := range srv.ListUsers() {
 | 
						for _, u := range srv.ListUsers() {
 | 
				
			||||||
		fmt.Fprintln(rw, "User:", u.name)
 | 
							fmt.Fprintln(rw, u)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Fprintln(rw, "Connected Users")
 | 
				
			||||||
	for _, u := range srv.ListConnectedUsers() {
 | 
						for _, u := range srv.ListConnectedUsers() {
 | 
				
			||||||
		fmt.Fprintln(rw, "Conn:", u.name)
 | 
							fmt.Fprintln(rw, u)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user