Compare commits

...

37 Commits
v0.0.1 ... main

Author SHA1 Message Date
Aaron Bieber e3b3dad3ee clean our passPath for good measure 2021-12-10 15:50:28 -07:00
Aaron Bieber 14bdf17e49 Add version flag 2021-12-10 15:50:07 -07:00
Aaron Bieber 0ea90c5ddd switch to term package 2021-12-10 15:49:52 -07:00
Aaron Bieber a86c07b5e8 add goreleaser config 2021-12-10 15:49:29 -07:00
Aaron Bieber d11f802f05
Merge pull request #5
Adds header authentication
2021-12-10 06:52:44 -07:00
Aaron Bieber e0ee714d3e
Merge pull request #8 from qbit/TW_5.2.1
update TW to 5.2.1
2021-12-10 06:35:16 -07:00
Aaron Bieber db7f06dc5b
Merge pull request #7 from qbit/muxd-handlers
Store our handlers in a slice and lock them when they are being used.
2021-12-10 06:34:42 -07:00
Aaron Bieber 68c586f805 use RWLock on handlers, fix close bug 2021-12-10 06:27:27 -07:00
Aaron Bieber 8c113d95c1 tidy 2021-12-10 06:26:31 -07:00
Aaron Bieber 1112c9874e update TW to 5.2.1 2021-12-09 06:28:37 -07:00
Aaron Bieber 18a4ed5026 remove extra pledges var 2021-12-09 05:59:56 -07:00
Aaron Bieber f9de42e801 Don't assign var 2021-12-09 05:48:34 -07:00
Aaron Bieber 4fdac0116f Store our handlers in a slice and lock them when they are being used.
Hopefully this helps with #3 (at least the HTTP 423 errors).
2021-12-09 05:16:00 -07:00
Aaron Bieber 0205c58868
Merge pull request #6 from bentsai/patch-1
Fix typo
2021-12-08 19:12:38 -07:00
Ben Tsai 4e224c9c08
Fix typo 2021-12-08 21:11:22 -05:00
Ysbrand van Eijck fc9da8e1f3
Adds missing break in header authentication 2021-12-08 13:48:13 +01:00
Ysbrand van Eijck 3ddc711cc1
Adds header authentication 2021-12-08 13:42:48 +01:00
Aaron Bieber 6e2f8a7495 Add new examples and expand installation info 2021-12-06 06:13:24 -07:00
Aaron Bieber 2551d2d9a0 remove useless []byte() 2021-12-06 06:07:35 -07:00
Aaron Bieber 3fb82cec02 update deps 2021-12-06 06:00:04 -07:00
Aaron Bieber 574110c52f update to TW 5.2.0 2021-12-06 05:45:41 -07:00
Aaron Bieber 3849f2ba51 switch to using ReducePledges 2021-07-08 19:05:28 -06:00
Aaron Bieber 3deceffa01 add some missing pledges 2021-05-20 18:42:15 -06:00
Aaron Bieber 7f03d5e16e ratched down some needed unveils 2021-05-20 18:33:35 -06:00
Aaron Bieber 7b16296562 comment to make it clear what is going on 2021-05-20 06:51:01 -06:00
Aaron Bieber 34876ea93d add htpasswd management to readme 2021-05-20 06:48:58 -06:00
Aaron Bieber 0c90ee2be7 Add the ability to manage .htpasswd from widdler. 2021-05-20 06:47:38 -06:00
Aaron Bieber 2cfd965467 +blurb about auth 2021-05-20 06:28:11 -06:00
Aaron Bieber 8f69c90a69 add features list to readme 2021-05-20 06:26:49 -06:00
Aaron Bieber 85d4c11447 Add TLS support. 2021-05-20 06:23:18 -06:00
Aaron Bieber 62ad6c78d6 mark multi-user complete 2021-05-19 21:20:12 -06:00
Aaron Bieber 4ce5ad6a00 handle the template execution error 2021-05-19 21:15:46 -06:00
Aaron Bieber e38b3cbc23 Add a landing page with instructions on how to create a new wiki 2021-05-19 21:14:22 -06:00
Aaron Bieber 490a1a757a Add support for multiple users \o/
Create a webdav and file handler for each user, lock them into a
directory that is their user name.
2021-05-19 20:30:45 -06:00
Aaron Bieber b3b8aa563d print full url 2021-05-18 16:57:33 -06:00
Aaron Bieber 769dac74ea Add the ability to disable http auth.
While here use better names fofr the flags
2021-05-18 16:34:49 -06:00
Aaron Bieber 723e91e05d link to running instance 2021-05-17 16:04:56 -06:00
8 changed files with 3407 additions and 12979 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
widdler
.htpasswd
dist/

55
.goreleaser.yml Normal file
View File

@ -0,0 +1,55 @@
before:
hooks:
- go mod tidy
- go vet
- staticcheck
- gosec .
builds:
- env:
- CGO_ENABLED=0
targets:
- openbsd_amd64
- openbsd_arm64
- openbsd_386
- openbsd_arm
- freebsd_amd64
- freebsd_arm64
- freebsd_386
- darwin_amd64
- darwin_arm64
- windows_amd64
- netbsd_amd64
- netbsd_arm64
- netbsd_386
- dragonfly_amd64
goos:
- openbsd
- freebsd
- netbsd
- dragonfly
- linux
- windows
- darwin
ldflags:
- -s -w -X main.build={{.Version}}
archives:
- replacements:
openbsd: OpenBSD
freebsd: FreeBSD
netbsd: NetBSD
dragonfly: DragonflyBSD
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'

View File

@ -4,26 +4,45 @@ widdler
widdler is a single binary that serves up
[TiddlyWiki](https://tiddlywiki.com)s.
It can be used to server existing wikis, or to create new ones.
It can be used to serve existing wikis, or to create new ones.
# Features
- TiddlyWikis are served over WebDav so you can save directly from the browser.
- Automatically create new wiki files by browsing to a non-existent html file.
- Built in .htpasswd management (Adding users).
- Password protection via HTTP Basic Authentication.
- Multiple users (adding another user to the .htaccess file creates a new user
namespace).
- Optional TLS support.
# Installation
For Go 1.16:
```
go get -u suah.dev/widdler
```
For Go 1.17 and up:
```
go install suah.dev/widdler@latest
```
# Running
```
mkdir wiki
cd wiki
# OpenBSD:
htpasswd .htpasswd youruser
# or on Linux/macOS
# htpasswd -c -B youruser
widdler
# Generate a .htpasswd file:
widdler -gen
Username: qbit
Passwd: ******
# Start the server
./widdler
```
Now open your browser to [http://localhost:8080](http://localhost:8080).
# Creating a new TiddlyWiki
Simply browse to the file name you wish to create. widdler will automatically
@ -33,5 +52,16 @@ create the wiki file based off the current `empty.html` TiddlyWiki version.
Simply hit the save button!
# TODO
- [ ] Multi-user support
# Updating widdler
```
go install suah.dev/widdler@latest
```
# Running without .htpasswd
You can disable auth all together by setting the `-auth` flag to false:
```
widdler -auth=false -wikis ~/wiki
```

File diff suppressed because one or more lines are too long

3007
empty-5.2.1.html Normal file

File diff suppressed because one or more lines are too long

8
go.mod
View File

@ -3,7 +3,9 @@ module suah.dev/widdler
go 1.16
require (
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210510120150-4163338589ed
suah.dev/protect v1.0.0
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
suah.dev/protect v1.2.0
)

24
go.sum
View File

@ -1,16 +1,18 @@
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211205041911-012df41ee64c h1:7SfqwP5fxEtl/P02w5IhKc86ziJ+A25yFrkVgoy2FT8=
golang.org/x/net v0.0.0-20211205041911-012df41ee64c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
suah.dev/protect v1.0.0 h1:X8pzDvDIZIiugmkmr6DES6JFO1XUdJWi34Ffmk6CMZY=
suah.dev/protect v1.0.0/go.mod h1:ZSgyBM30JUwhVPWJzVHh0jlu5W6Qz1VR6tIhAzqJZ9Y=
suah.dev/protect v1.2.0 h1:4G4V43yVYXCjLFzaE9QJR0fLo3rf5vNBS9YxyoI19DU=
suah.dev/protect v1.2.0/go.mod h1:Ocn1yqUskqe/is6N2bxQxtT+fegbvQsOFyHbJAQu9XE=

344
main.go
View File

@ -1,6 +1,7 @@
package main
import (
"crypto/tls"
"embed"
"encoding/csv"
"flag"
@ -14,25 +15,82 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"time"
"golang.org/x/crypto/bcrypt"
"golang.org/x/net/webdav"
"golang.org/x/term"
"suah.dev/protect"
)
var twFile = "empty-5.1.23.html"
// Landing will be used to fill our landing template
type Landing struct {
User string
URL string
}
//go:embed empty-5.1.23.html
var tiddly embed.FS
const landingPage = `
<h1>Hello{{if .User}} {{.User}}{{end}}! Welcome to widdler!</h1>
<p>To create a new TiddlyWiki html file, simply append an html file name to the URL in the address bar!</p>
<h3>For example:</h3>
<a href="{{.URL}}">{{.URL}}</a>
<p>This will create a new wiki called "<b>wiki.html</b>"</p>
<p>After creating a wiki, this message will be replaced by a list of your wiki files.</p>
`
var (
davDir string
listen string
passPath string
users map[string]string
twFile = "empty-5.2.1.html"
//go:embed empty-5.2.1.html
tiddly embed.FS
templ *template.Template
)
type userHandler struct {
mu sync.Mutex
dav *webdav.Handler
fs http.Handler
name string
}
type userHandlers struct {
list []userHandler
mu sync.RWMutex
}
func (u *userHandlers) find(name string) *userHandler {
for i := range u.list {
if u.list[i].name == name {
return &u.list[i]
}
}
return nil
}
var (
auth string
davDir string
fullListen string
genHtpass bool
handlers userHandlers
listen string
passPath string
tlsCert string
tlsKey string
users map[string]string
version bool
build string
)
var pledges = "stdio wpath rpath cpath tty inet dns unveil"
func init() {
users = make(map[string]string)
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
@ -40,36 +98,26 @@ func init() {
log.Fatalln(err)
}
flag.StringVar(&davDir, "davdir", dir, "Directory to serve over WebDAV.")
flag.StringVar(&listen, "http", "127.0.0.1:8080", "Listen on")
flag.StringVar(&davDir, "wikis", dir, "Directory of TiddlyWikis to serve over WebDAV.")
flag.StringVar(&listen, "http", "localhost:8080", "Listen on")
flag.StringVar(&tlsCert, "tlscert", "", "TLS certificate.")
flag.StringVar(&tlsKey, "tlskey", "", "TLS key.")
flag.StringVar(&passPath, "htpass", fmt.Sprintf("%s/.htpasswd", dir), "Path to .htpasswd file..")
flag.StringVar(&auth, "auth", "basic", "Enable HTTP Basic Authentication.")
flag.BoolVar(&genHtpass, "gen", false, "Generate a .htpasswd file or add a new entry to an existing file.")
flag.BoolVar(&version, "v", false, "Show version and exit.")
flag.Parse()
// These are OpenBSD specific protections used to prevent unnecessary file access.
_ = protect.Unveil(passPath, "r")
_ = protect.Unveil(passPath, "rwc")
_ = protect.Unveil(davDir, "rwc")
_ = protect.Unveil("/etc/ssl/cert.pem", "r")
_ = protect.Unveil("/etc/resolv.conf", "r")
_ = protect.Pledge("stdio wpath rpath cpath inet dns")
_ = protect.Pledge(pledges)
p, err := os.Open(passPath)
templ, err = template.New("landing").Parse(landingPage)
if err != nil {
log.Fatal(err)
}
defer p.Close()
ht := csv.NewReader(p)
ht.Comma = ':'
ht.Comment = '#'
ht.TrimLeadingSpace = true
entries, err := ht.ReadAll()
if err != nil {
log.Fatal(err)
}
for _, parts := range entries {
users[parts[0]] = parts[1]
log.Fatalln(err)
}
}
@ -104,7 +152,7 @@ func createEmpty(path string) error {
if os.IsNotExist(fErr) {
log.Printf("creating %q\n", path)
twData, _ := tiddly.ReadFile(twFile)
wErr := ioutil.WriteFile(path, []byte(twData), 0600)
wErr := ioutil.WriteFile(path, twData, 0600)
if wErr != nil {
return wErr
}
@ -112,43 +160,190 @@ func createEmpty(path string) error {
return nil
}
func prompt(prompt string, secure bool) (string, error) {
var input string
fmt.Print(prompt)
if secure {
b, err := term.ReadPassword(int(os.Stdin.Fd()))
if err != nil {
return "", err
}
input = string(b)
} else {
_, err := fmt.Scanln(&input)
if err != nil {
return "", err
}
}
return input, nil
}
func main() {
wdav := &webdav.Handler{
LockSystem: webdav.NewMemLS(),
FileSystem: webdav.Dir(davDir),
if version {
fmt.Println(build)
os.Exit(0)
}
if genHtpass {
user, err := prompt("Username: ", false)
if err != nil {
log.Fatalln(err)
}
pass, err := prompt("Password: ", true)
if err != nil {
log.Fatalln(err)
}
hash, err := bcrypt.GenerateFromPassword([]byte(pass), 11)
if err != nil {
log.Fatalln(err)
}
f, err := os.OpenFile(filepath.Clean(passPath), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
log.Fatalln(err)
}
if _, err := f.WriteString(fmt.Sprintf("%s:%s\n", user, hash)); err != nil {
log.Fatalln(err)
}
err = f.Close()
if err != nil {
log.Fatalln(err)
}
fmt.Printf("Added %q to %q\n", user, passPath)
os.Exit(0)
}
pledges, _ = protect.ReducePledges(pledges, "tty")
// drop to only read on passPath
_ = protect.Unveil(passPath, "r")
pledges, _ = protect.ReducePledges(pledges, "unveil")
_, fErr := os.Stat(passPath)
if os.IsNotExist(fErr) {
if auth == "basic" || auth == "header" {
fmt.Println("No .htpasswd file found!")
os.Exit(1)
}
} else {
p, err := os.Open(filepath.Clean(passPath))
if err != nil {
log.Fatal(err)
}
ht := csv.NewReader(p)
ht.Comma = ':'
ht.Comment = '#'
ht.TrimLeadingSpace = true
entries, err := ht.ReadAll()
if err != nil {
log.Fatal(err)
}
err = p.Close()
if err != nil {
log.Fatal(err)
}
for _, parts := range entries {
users[parts[0]] = parts[1]
}
}
idxHandler := http.FileServer(http.Dir(davDir))
if auth == "basic" || auth == "header" {
for u := range users {
uPath := path.Join(davDir, u)
handlers.list = append(handlers.list, userHandler{
name: u,
dav: &webdav.Handler{
LockSystem: webdav.NewMemLS(),
FileSystem: webdav.Dir(uPath),
},
fs: http.FileServer(http.Dir(uPath)),
})
}
} else {
handlers.list = append(handlers.list, userHandler{
name: "",
dav: &webdav.Handler{
LockSystem: webdav.NewMemLS(),
FileSystem: webdav.Dir(davDir),
},
fs: http.FileServer(http.Dir(davDir)),
})
}
mux := http.NewServeMux()
mux.HandleFunc("/", logger(func(w http.ResponseWriter, r *http.Request) {
user, pass := "", ""
var ok bool
if strings.Contains(r.URL.Path, ".htpasswd") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
http.NotFound(w, r)
return
}
user, pass, ok := r.BasicAuth()
if !(ok && authenticate(user, pass)) {
w.Header().Set("WWW-Authenticate", `Basic realm="davfs"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
// Prevent directory traversal
if strings.Contains(r.URL.Path, "..") {
http.NotFound(w, r)
return
}
//wdav.Prefix = user
fp := path.Join(davDir, r.URL.Path)
/*
fp := path.Join(davDir, user, r.URL.Path)
_, dErr := os.Stat(user)
if os.IsNotExist(dErr) {
mErr := os.Mkdir(path.Join(davDir, user), 0700)
if mErr != nil {
http.Error(w, mErr.Error(), http.StatusInternalServerError)
return
if auth == "basic" {
user, pass, ok = r.BasicAuth()
if !(ok && authenticate(user, pass)) {
w.Header().Set("WWW-Authenticate", `Basic realm="widdler"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
} else if auth == "header" {
var prefix = "Auth"
for name, values := range r.Header {
if strings.HasPrefix(name, prefix) {
user = strings.TrimLeft(name, prefix)
pass = values[0]
ok = true
break
}
}
*/
if !(ok && authenticate(user, pass)) {
w.Header().Set("WWW-Authenticate", `Basic realm="widdler"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
}
handlers.mu.RLock()
handler := handlers.find(user)
handlers.mu.RUnlock()
if handler == nil {
http.NotFound(w, r)
return
}
handler.mu.Lock()
defer handler.mu.Unlock()
userPath := path.Join(davDir, user)
fullPath := path.Join(davDir, user, r.URL.Path)
_, dErr := os.Stat(userPath)
if os.IsNotExist(dErr) {
mErr := os.Mkdir(userPath, 0700)
if mErr != nil {
http.Error(w, mErr.Error(), http.StatusInternalServerError)
return
}
}
isHTML, err := regexp.Match(`\.html$`, []byte(r.URL.Path))
if err != nil {
@ -158,17 +353,37 @@ func main() {
if isHTML {
// HTML files will be created or sent back
err := createEmpty(fp)
err := createEmpty(fullPath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
wdav.ServeHTTP(w, r)
handler.dav.ServeHTTP(w, r)
} else {
// Everything else is browsable
idxHandler.ServeHTTP(w, r)
return
entries, err := os.ReadDir(userPath)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(entries) > 0 {
handler.fs.ServeHTTP(w, r)
} else {
l := Landing{
URL: fmt.Sprintf("%s/wiki.html", fullListen),
}
if user != "" {
l.User = user
}
err = templ.ExecuteTemplate(w, "landing", l)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
}))
@ -178,9 +393,24 @@ func main() {
lis, err := net.Listen("tcp", listen)
if err != nil {
log.Panic(err)
log.Fatalln(err)
}
log.Printf("Listening for HTTP on '%s'", listen)
log.Panic(s.Serve(lis))
if tlsCert != "" && tlsKey != "" {
fullListen = fmt.Sprintf("https://%s", listen)
s.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
}
log.Printf("Listening for HTTPS on 'https://%s'", listen)
log.Fatalln(s.ServeTLS(lis, tlsCert, tlsKey))
} else {
fullListen = fmt.Sprintf("http://%s", listen)
log.Printf("Listening for HTTP on 'http://%s'", listen)
log.Fatalln(s.Serve(lis))
}
}