diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 008bca6..11d1658 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,8 +32,8 @@ jobs: - name: Test run: go test . - working-directory: . + working-directory: cmd/simplehttpserver/ - name: Build run: go build . - working-directory: . + working-directory: cmd/simplehttpserver/ diff --git a/.goreleaser.yml b/.goreleaser.yml index 50bb74f..f6cb1e2 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,6 +1,6 @@ builds: - binary: simplehttpserver - main: simplehttpserver.go + main: cmd/simplehttpserver/simplehttpserver.go goos: - linux - windows diff --git a/Dockerfile b/Dockerfile index a01590c..4603b20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM golang:1.14-alpine AS builder +FROM golang:1.16-alpine AS builder RUN apk add --no-cache git -RUN GO111MODULE=auto go get -u -v github.com/projectdiscovery/simplehttpserver +RUN GO111MODULE=auto go get -u -v github.com/projectdiscovery/simplehttpserver/cmd/simplehttpserver FROM alpine:latest COPY --from=builder /go/bin/simplehttpserver /usr/local/bin/ diff --git a/gen_cert.sh b/gen_cert.sh deleted file mode 100644 index 0cba96a..0000000 --- a/gen_cert.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -openssl genrsa -out server.key 2048 -openssl ecparam -genkey -name secp384r1 -out server.key -openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 \ No newline at end of file diff --git a/internal/runner/banner.go b/internal/runner/banner.go new file mode 100644 index 0000000..c8972db --- /dev/null +++ b/internal/runner/banner.go @@ -0,0 +1,24 @@ +package runner + +import "github.com/projectdiscovery/gologger" + +const banner = ` + _ _ _ _ _ + ___(_)_ __ ___ _ __ | | ___| |__ | |_| |_ _ __ ___ ___ _ ____ _____ _ __ +/ __| | '_ ' _ \| '_ \| |/ _ \ '_ \| __| __| '_ \/ __|/ _ \ '__\ \ / / _ \ '__| +\__ \ | | | | | | |_) | | __/ | | | |_| |_| |_) \__ \ __/ | \ V / __/ | +|___/_|_| |_| |_| .__/|_|\___|_| |_|\__|\__| .__/|___/\___|_| \_/ \___|_| + |_| |_| +` + +// Version is the current version +const Version = `0.0.1` + +// showBanner is used to show the banner to the user +func showBanner() { + gologger.Print().Msgf("%s\n", banner) + gologger.Print().Msgf("\t\tprojectdiscovery.io\n\n") + + gologger.Print().Msgf("Use with caution. You are responsible for your actions\n") + gologger.Print().Msgf("Developers assume no liability and are not responsible for any misuse or damage.\n") +} diff --git a/internal/runner/doc.go b/internal/runner/doc.go new file mode 100644 index 0000000..6d6e364 --- /dev/null +++ b/internal/runner/doc.go @@ -0,0 +1,2 @@ +// Package runner contains the internal logic +package runner diff --git a/internal/runner/options.go b/internal/runner/options.go new file mode 100644 index 0000000..3170cd3 --- /dev/null +++ b/internal/runner/options.go @@ -0,0 +1,93 @@ +package runner + +import ( + "flag" + "os" + "strings" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/gologger/levels" +) + +type Options struct { + ListenAddress string + Folder string + BasicAuth string + username string + password string + Realm string + TLSCertificate string + TLSKey string + TLSDomain string + HTTPS bool + Verbose bool + EnableUpload bool + EnableTCP bool + RulesFile string + TCPWithTLS bool + Version bool + Silent bool +} + +// ParseOptions parses the command line options for application +func ParseOptions() *Options { + options := &Options{} + flag.StringVar(&options.ListenAddress, "listen", "0.0.0.0:8000", "Address:Port") + flag.BoolVar(&options.EnableTCP, "tcp", false, "TCP Server") + flag.BoolVar(&options.TCPWithTLS, "tls", false, "Enable TCP TLS") + flag.StringVar(&options.RulesFile, "rules", "", "Rules yaml file") + flag.StringVar(&options.Folder, "path", ".", "Folder") + flag.BoolVar(&options.EnableUpload, "upload", false, "Enable upload via PUT") + flag.BoolVar(&options.HTTPS, "https", false, "HTTPS") + flag.StringVar(&options.TLSCertificate, "cert", "", "HTTPS Certificate") + flag.StringVar(&options.TLSKey, "key", "", "HTTPS Certificate Key") + flag.StringVar(&options.TLSDomain, "domain", "local.host", "Domain") + flag.BoolVar(&options.Verbose, "verbose", false, "Verbose") + flag.StringVar(&options.BasicAuth, "basic-auth", "", "Basic auth (username:password)") + flag.StringVar(&options.Realm, "realm", "Please enter username and password", "Realm") + flag.BoolVar(&options.Version, "version", false, "Show version of the software") + flag.BoolVar(&options.Silent, "silent", false, "Show only results in the output") + + flag.Parse() + + // Read the inputs and configure the logging + options.configureOutput() + + showBanner() + + if options.Version { + gologger.Info().Msgf("Current Version: %s\n", Version) + os.Exit(0) + } + + options.validateOptions() + + return options +} + +func (options *Options) validateOptions() { + if flag.NArg() > 0 && options.Folder == "." { + options.Folder = flag.Args()[0] + } + + if options.BasicAuth != "" { + baTokens := strings.SplitN(options.BasicAuth, ":", 2) + if len(baTokens) > 0 { + options.username = baTokens[0] + } + if len(baTokens) > 1 { + options.password = baTokens[1] + } + } +} + +// configureOutput configures the output on the screen +func (options *Options) configureOutput() { + // If the user desires verbose output, show verbose output + if options.Verbose { + gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose) + } + if options.Silent { + gologger.DefaultLogger.SetMaxLevel(levels.LevelSilent) + } +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go new file mode 100644 index 0000000..a5d5230 --- /dev/null +++ b/internal/runner/runner.go @@ -0,0 +1,70 @@ +package runner + +import ( + "github.com/projectdiscovery/simplehttpserver/pkg/httpserver" + "github.com/projectdiscovery/simplehttpserver/pkg/tcpserver" +) + +// Runner is a client for running the enumeration process. +type Runner struct { + options *Options + serverTCP *tcpserver.TCPServer + httpServer *httpserver.HTTPServer +} + +func New(options *Options) (*Runner, error) { + r := Runner{options: options} + if r.options.EnableTCP { + serverTCP, err := tcpserver.New(tcpserver.Options{ + Listen: r.options.ListenAddress, + TLS: r.options.TCPWithTLS, + Domain: "local.host", + Verbose: r.options.Verbose, + }) + if err != nil { + return nil, err + } + err = serverTCP.LoadTemplate(r.options.RulesFile) + if err != nil { + return nil, err + } + r.serverTCP = serverTCP + return &r, nil + } + + httpServer, err := httpserver.New(&httpserver.Options{ + Folder: r.options.Folder, + EnableUpload: r.options.EnableUpload, + ListenAddress: r.options.ListenAddress, + TLS: r.options.HTTPS, + Certificate: r.options.TLSCertificate, + CertificateKey: r.options.TLSKey, + CertificateDomain: r.options.TLSDomain, + BasicAuthUsername: r.options.username, + BasicAuthPassword: r.options.password, + BasicAuthReal: r.options.Realm, + Verbose: r.options.Verbose, + }) + if err != nil { + return nil, err + } + r.httpServer = httpServer + + return &r, nil +} + +func (r *Runner) Run() error { + if r.options.EnableTCP { + return r.serverTCP.ListenAndServe() + } + + if r.options.HTTPS { + return r.httpServer.ListenAndServeTLS() + } + + return r.httpServer.ListenAndServe() +} + +func (r *Runner) Close() error { + return nil +} diff --git a/pkg/httpserver/authlayer.go b/pkg/httpserver/authlayer.go new file mode 100644 index 0000000..f2eff4b --- /dev/null +++ b/pkg/httpserver/authlayer.go @@ -0,0 +1,19 @@ +package httpserver + +import ( + "fmt" + "net/http" +) + +func (t *HTTPServer) basicauthlayer(handler http.Handler) http.HandlerFunc { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok || user != t.options.BasicAuthUsername || pass != t.options.BasicAuthPassword { + w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", t.options.BasicAuthReal)) + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("Unauthorized.\n")) //nolint + return + } + handler.ServeHTTP(w, r) + }) +} diff --git a/pkg/httpserver/httpserver.go b/pkg/httpserver/httpserver.go new file mode 100644 index 0000000..6e817d3 --- /dev/null +++ b/pkg/httpserver/httpserver.go @@ -0,0 +1,120 @@ +package httpserver + +import ( + "errors" + "net" + "net/http" + "os" + "runtime" + "strconv" + "syscall" + + "github.com/projectdiscovery/gologger" + "github.com/projectdiscovery/simplehttpserver/pkg/sslcert" +) + +type Options struct { + Folder string + EnableUpload bool + ListenAddress string + TLS bool + Certificate string + CertificateKey string + CertificateDomain string + BasicAuthUsername string + BasicAuthPassword string + BasicAuthReal string + Verbose bool +} + +type HTTPServer struct { + options *Options + layers http.Handler + listener net.Listener +} + +func New(options *Options) (*HTTPServer, error) { + var h HTTPServer + EnableUpload = options.EnableUpload + EnableVerbose = options.Verbose + layers := h.loglayer(http.FileServer(http.Dir(options.Folder))) + if options.BasicAuthUsername != "" || options.BasicAuthPassword != "" { + layers = h.loglayer(h.basicauthlayer(http.FileServer(http.Dir(options.Folder)))) + } + + return &HTTPServer{options: options, layers: layers}, nil +} + +func (t *HTTPServer) ListenAndServe() error { + var err error +retry_listen: + gologger.Print().Msgf("Serving %s on http://%s/...", t.options.Folder, t.options.ListenAddress) + err = http.ListenAndServe(t.options.ListenAddress, t.layers) + if err != nil { + if isErrorAddressAlreadyInUse(err) { + gologger.Print().Msgf("Can't listen on %s: %s - retrying with another port\n", t.options.ListenAddress, err) + newListenAddress, err := incPort(t.options.ListenAddress) + if err != nil { + return err + } + t.options.ListenAddress = newListenAddress + goto retry_listen + } + } + return nil +} + +func (t *HTTPServer) ListenAndServeTLS() error { + gologger.Print().Msgf("Serving %s on https://%s/...", t.options.Folder, t.options.ListenAddress) + if t.options.Certificate == "" || t.options.CertificateKey == "" { + tlsOptions := sslcert.DefaultOptions + tlsOptions.Host = t.options.CertificateDomain + tlsConfig, err := sslcert.NewTLSConfig(tlsOptions) + if err != nil { + return err + } + httpServer := &http.Server{ + Addr: t.options.ListenAddress, + TLSConfig: tlsConfig, + } + httpServer.Handler = t.layers + return httpServer.ListenAndServeTLS("", "") + } + return http.ListenAndServeTLS(t.options.ListenAddress, t.options.Certificate, t.options.CertificateKey, t.layers) +} + +func isErrorAddressAlreadyInUse(err error) bool { + var eOsSyscall *os.SyscallError + if !errors.As(err, &eOsSyscall) { + return false + } + var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) + if !errors.As(eOsSyscall, &errErrno) { + return false + } + if errErrno == syscall.EADDRINUSE { + return true + } + const WSAEADDRINUSE = 10048 + if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { + return true + } + return false +} + +func incPort(address string) (string, error) { + addrOrig, portOrig, err := net.SplitHostPort(address) + if err != nil { + return address, err + } + + // increment port + portNumber, err := strconv.Atoi(portOrig) + if err != nil { + return address, err + } + portNumber++ + newPort := strconv.FormatInt(int64(portNumber), 10) + + return net.JoinHostPort(addrOrig, newPort), nil +} diff --git a/pkg/httpserver/loglayer.go b/pkg/httpserver/loglayer.go new file mode 100644 index 0000000..2d84d9a --- /dev/null +++ b/pkg/httpserver/loglayer.go @@ -0,0 +1,69 @@ +package httpserver + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/http/httputil" + "path" + + "github.com/projectdiscovery/gologger" +) + +// Convenience globals +var ( + EnableUpload bool + EnableVerbose bool +) + +func (t *HTTPServer) loglayer(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fullRequest, _ := httputil.DumpRequest(r, true) + lrw := newLoggingResponseWriter(w) + handler.ServeHTTP(lrw, r) + + // Handles file write if enabled + if EnableUpload && r.Method == http.MethodPut { + data, err := ioutil.ReadAll(r.Body) + if err != nil { + gologger.Print().Msgf("%s\n", err) + } + err = handleUpload(path.Base(r.URL.Path), data) + if err != nil { + gologger.Print().Msgf("%s\n", err) + } + } + + if EnableVerbose { + headers := new(bytes.Buffer) + lrw.Header().Write(headers) //nolint + gologger.Print().Msgf("\nRemote Address: %s\n%s\n%s %d %s\n%s\n%s\n", r.RemoteAddr, string(fullRequest), r.Proto, lrw.statusCode, http.StatusText(lrw.statusCode), headers.String(), string(lrw.Data)) + } else { + gologger.Print().Msgf("%s \"%s %s %s\" %d %d", r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, len(lrw.Data)) + } + }) +} + +type loggingResponseWriter struct { + http.ResponseWriter + statusCode int + Data []byte +} + +func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { + return &loggingResponseWriter{w, http.StatusOK, []byte{}} +} + +func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { + lrw.Data = append(lrw.Data, data...) + return lrw.ResponseWriter.Write(data) +} + +func (lrw *loggingResponseWriter) Header() http.Header { + return lrw.ResponseWriter.Header() +} + +func (lrw *loggingResponseWriter) WriteHeader(code int) { + lrw.statusCode = code + lrw.ResponseWriter.WriteHeader(code) +} diff --git a/pkg/httpserver/uploadlayer.go b/pkg/httpserver/uploadlayer.go new file mode 100644 index 0000000..2663fba --- /dev/null +++ b/pkg/httpserver/uploadlayer.go @@ -0,0 +1,7 @@ +package httpserver + +import "io/ioutil" + +func handleUpload(file string, data []byte) error { + return ioutil.WriteFile(file, data, 0655) +} diff --git a/pkg/tcpserver/tcpserver.go b/pkg/tcpserver/tcpserver.go index fc08f03..15be615 100644 --- a/pkg/tcpserver/tcpserver.go +++ b/pkg/tcpserver/tcpserver.go @@ -6,6 +6,7 @@ import ( "net" "time" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/simplehttpserver/pkg/sslcert" "gopkg.in/yaml.v2" ) @@ -17,6 +18,7 @@ type Options struct { Key string Domain string rules []Rule + Verbose bool } type TCPServer struct { @@ -34,6 +36,7 @@ func (t *TCPServer) AddRule(rule Rule) error { } func (t *TCPServer) ListenAndServe() error { + gologger.Print().Msgf("Serving %s on tcp://%s", t.options.Listen) listener, err := net.Listen("tcp4", t.options.Listen) if err != nil { return err @@ -53,12 +56,16 @@ func (t *TCPServer) handleConnection(conn net.Conn) error { return err } + gologger.Print().Msgf("%s\n", buf) + resp, err := t.BuildResponse(buf) if err != nil { return err } conn.Write(resp) + + gologger.Print().Msgf("%s\n", resp) } } diff --git a/simplehttpserver.go b/simplehttpserver.go deleted file mode 100644 index f03f258..0000000 --- a/simplehttpserver.go +++ /dev/null @@ -1,233 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/http/httputil" - "os" - "path" - "runtime" - "strconv" - "strings" - "syscall" - - "github.com/projectdiscovery/gologger" - "github.com/projectdiscovery/simplehttpserver/pkg/sslcert" - "github.com/projectdiscovery/simplehttpserver/pkg/tcpserver" -) - -type options struct { - ListenAddress string - Folder string - BasicAuth string - username string - password string - Realm string - Certificate string - Key string - Domain string - HTTPS bool - Verbose bool - Upload bool - TCP bool - RulesFile string - TLS bool -} - -var opts options - -func main() { - flag.StringVar(&opts.ListenAddress, "listen", "0.0.0.0:8000", "Address:Port") - flag.BoolVar(&opts.TCP, "tcp", false, "TCP Server") - flag.BoolVar(&opts.TLS, "tls", false, "Enable TCP TLS") - flag.StringVar(&opts.RulesFile, "rules", "", "Rules yaml file") - flag.StringVar(&opts.Folder, "path", ".", "Folder") - flag.BoolVar(&opts.Upload, "upload", false, "Enable upload via PUT") - flag.BoolVar(&opts.HTTPS, "https", false, "HTTPS") - flag.StringVar(&opts.Certificate, "cert", "", "Certificate") - flag.StringVar(&opts.Key, "key", "", "Key") - flag.StringVar(&opts.Domain, "domain", "local.host", "Domain") - flag.BoolVar(&opts.Verbose, "v", false, "Verbose") - flag.StringVar(&opts.BasicAuth, "basic-auth", "", "Basic auth (username:password)") - flag.StringVar(&opts.Realm, "realm", "Please enter username and password", "Realm") - - flag.Parse() - - if flag.NArg() > 0 && opts.Folder == "." { - opts.Folder = flag.Args()[0] - } - - if opts.TCP { - serverTCP, err := tcpserver.New(tcpserver.Options{Listen: opts.ListenAddress, TLS: opts.TLS, Domain: "local.host"}) - if err != nil { - gologger.Fatal().Msgf("%s\n", err) - } - err = serverTCP.LoadTemplate(opts.RulesFile) - if err != nil { - gologger.Fatal().Msgf("%s\n", err) - } - - gologger.Print().Msgf("%s\n", serverTCP.ListenAndServe()) - } - - gologger.Print().Msgf("Serving %s on http://%s/...", opts.Folder, opts.ListenAddress) - layers := loglayer(http.FileServer(http.Dir(opts.Folder))) - if opts.BasicAuth != "" { - baTokens := strings.SplitN(opts.BasicAuth, ":", 2) - if len(baTokens) > 0 { - opts.username = baTokens[0] - } - if len(baTokens) > 1 { - opts.password = baTokens[1] - } - layers = loglayer(basicauthlayer(http.FileServer(http.Dir(opts.Folder)))) - } - if opts.Upload { - gologger.Print().Msg("Starting service with Upload enabled") - } -retry_listen: - gologger.Print().Msgf("Serving %s on http://%s/...", opts.Folder, opts.ListenAddress) - var err error - if opts.HTTPS { - if opts.Certificate == "" || opts.Key == "" { - tlsOptions := sslcert.DefaultOptions - tlsOptions.Host = opts.Domain - tlsConfig, err := sslcert.NewTLSConfig(tlsOptions) - if err != nil { - gologger.Fatal().Msgf("%s\n", err) - } - httpServer := &http.Server{ - Addr: opts.ListenAddress, - TLSConfig: tlsConfig, - } - httpServer.Handler = layers - gologger.Print().Msgf("%s\n", httpServer.ListenAndServeTLS("", "")) - } else { - gologger.Print().Msgf("%s\n", http.ListenAndServeTLS(opts.ListenAddress, opts.Certificate, opts.Key, layers)) - } - } else { - err = http.ListenAndServe(opts.ListenAddress, layers) - } - if err != nil { - if isErrorAddressAlreadyInUse(err) { - gologger.Print().Msgf("Can't listen on %s: %s - retrying with another port\n", opts.ListenAddress, err) - newListenAddress, err := incPort(opts.ListenAddress) - if err != nil { - gologger.Fatal().Msgf("%s\n", err) - } - opts.ListenAddress = newListenAddress - goto retry_listen - } - gologger.Print().Msgf("%s\n", err) - } -} - -func loglayer(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fullRequest, _ := httputil.DumpRequest(r, true) - lrw := newLoggingResponseWriter(w) - handler.ServeHTTP(lrw, r) - - // Handles file write if enabled - if opts.Upload && r.Method == http.MethodPut { - data, err := ioutil.ReadAll(r.Body) - if err != nil { - gologger.Print().Msgf("%s\n", err) - } - err = handleUpload(path.Base(r.URL.Path), data) - if err != nil { - gologger.Print().Msgf("%s\n", err) - } - } - - if opts.Verbose { - headers := new(bytes.Buffer) - lrw.Header().Write(headers) //nolint - gologger.Print().Msgf("\nRemote Address: %s\n%s\n%s %d %s\n%s\n%s\n", r.RemoteAddr, string(fullRequest), r.Proto, lrw.statusCode, http.StatusText(lrw.statusCode), headers.String(), string(lrw.Data)) - } else { - gologger.Print().Msgf("%s \"%s %s %s\" %d %d", r.RemoteAddr, r.Method, r.URL, r.Proto, lrw.statusCode, len(lrw.Data)) - } - }) -} - -func basicauthlayer(handler http.Handler) http.HandlerFunc { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, pass, ok := r.BasicAuth() - if !ok || user != opts.username || pass != opts.password { - w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", opts.Realm)) - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("Unauthorized.\n")) //nolint - return - } - handler.ServeHTTP(w, r) - }) -} - -type loggingResponseWriter struct { - http.ResponseWriter - statusCode int - Data []byte -} - -func newLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter { - return &loggingResponseWriter{w, http.StatusOK, []byte{}} -} - -func (lrw *loggingResponseWriter) Write(data []byte) (int, error) { - lrw.Data = append(lrw.Data, data...) - return lrw.ResponseWriter.Write(data) -} - -func (lrw *loggingResponseWriter) Header() http.Header { - return lrw.ResponseWriter.Header() -} - -func (lrw *loggingResponseWriter) WriteHeader(code int) { - lrw.statusCode = code - lrw.ResponseWriter.WriteHeader(code) -} - -func handleUpload(file string, data []byte) error { - return ioutil.WriteFile(file, data, 0655) -} - -func isErrorAddressAlreadyInUse(err error) bool { - var eOsSyscall *os.SyscallError - if !errors.As(err, &eOsSyscall) { - return false - } - var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr) - if !errors.As(eOsSyscall, &errErrno) { - return false - } - if errErrno == syscall.EADDRINUSE { - return true - } - const WSAEADDRINUSE = 10048 - if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE { - return true - } - return false -} - -func incPort(address string) (string, error) { - addrOrig, portOrig, err := net.SplitHostPort(address) - if err != nil { - return address, err - } - - // increment port - portNumber, err := strconv.Atoi(portOrig) - if err != nil { - return address, err - } - portNumber++ - newPort := strconv.FormatInt(int64(portNumber), 10) - - return net.JoinHostPort(addrOrig, newPort), nil -}