diff --git a/README.md b/README.md index d5bd030a0f82435bd4a8529bacf57ddce76afeb2..cbefcd78454a8727921dd3365f298e54026f3724 100644 --- a/README.md +++ b/README.md @@ -158,32 +158,6 @@ ssh: # This is the address that will be used to clone repositories. public_url: "ssh://localhost:23231" - # The cross-origin request security options - cors: - # The allowed cross-origin headers - allowed_headers: - - Accept - - Accept-Language - - Content-Language - - Origin - # - Content-Type - # - X-Requested-With - # - User-Agent - # - Authorization - # - Access-Control-Request-Method - - # The allowed cross-origin URLs - # allowed_origins: - # - * - - # The allowed cross-origin methods - allowed_methods: - - GET - - HEAD - - POST - # - PUT - # - OPTIONS - # The path to the SSH server's private key. key_path: "ssh/soft_serve_host" diff --git a/cmd/soft/hook/hook.go b/cmd/soft/hook/hook.go index 43718ed22042e0c9d5c6b84cb9ac1488b3c724eb..e9b3f1f5c1ff6c3ebf40cbf3f5d5160c53ed8953 100644 --- a/cmd/soft/hook/hook.go +++ b/cmd/soft/hook/hook.go @@ -65,6 +65,8 @@ var ( // This is set in the server before invoking git-receive-pack/git-upload-pack repoName := os.Getenv("SOFT_SERVE_REPO_NAME") + logger := log.FromContext(ctx).With("repo", repoName) + stdin := cmd.InOrStdin() stdout := cmd.OutOrStdout() stderr := cmd.ErrOrStderr() @@ -80,9 +82,11 @@ var ( scanner := bufio.NewScanner(stdin) for scanner.Scan() { buf.Write(scanner.Bytes()) + buf.WriteByte('\n') fields := strings.Fields(scanner.Text()) if len(fields) != 3 { - return fmt.Errorf("invalid hook input: %s", scanner.Text()) + logger.Error(fmt.Sprintf("invalid %s hook input", cmdName), "input", scanner.Text()) + continue } opts = append(opts, hooks.HookArg{ OldSha: fields[0], @@ -99,7 +103,8 @@ var ( } case hooks.UpdateHook: if len(args) != 3 { - return fmt.Errorf("invalid update hook input: %s", args) + logger.Error("invalid update hook input", "input", args) + break } hks.Update(ctx, stdout, stderr, repoName, hooks.HookArg{ @@ -115,7 +120,7 @@ var ( if stat, err := os.Stat(customHookPath); err == nil && !stat.IsDir() && stat.Mode()&0o111 != 0 { // If the custom hook is executable, run it if err := runCommand(ctx, &buf, stdout, stderr, customHookPath, args...); err != nil { - return fmt.Errorf("failed to run custom hook: %w", err) + logger.Error("failed to run custom hook", "err", err) } } diff --git a/go.mod b/go.mod index 9fbd9d81d16b368c9651e59c7f1ffcf684cc1a7c..ffd590990293616db03614748deade271399e236 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/charmbracelet/glamour v0.9.1 + github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/dustin/go-humanize v1.0.1 github.com/go-git/go-git/v5 v5.14.0 github.com/matryer/is v1.4.1 @@ -22,15 +23,15 @@ require ( github.com/charmbracelet/bubbletea/v2 v2.0.0-alpha.2.0.20250318133619-2ce107c85fed github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92 - github.com/charmbracelet/keygen v0.5.1 + github.com/charmbracelet/keygen v0.5.3 github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250313133401-58331b1fff40 - github.com/charmbracelet/log v0.4.1-0.20241101171137-e66b83d34ed9 + github.com/charmbracelet/log v0.4.1 github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894 github.com/charmbracelet/wish/v2 v2.0.0-20250319172925-3ae4bfe9c24e github.com/charmbracelet/x/ansi v0.8.0 github.com/go-jose/go-jose/v3 v3.0.3 github.com/gobwas/glob v0.2.3 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/go-querystring v1.1.0 github.com/google/uuid v1.6.0 github.com/gorilla/handlers v1.5.2 @@ -45,7 +46,7 @@ require ( github.com/rogpeppe/go-internal v1.14.1 github.com/spf13/cobra v1.9.1 go.uber.org/automaxprocs v1.6.0 - golang.org/x/crypto v0.35.0 + golang.org/x/crypto v0.36.0 golang.org/x/sync v0.12.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.36.1 @@ -58,7 +59,6 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 // indirect github.com/charmbracelet/x/cellbuf v0.0.13 // indirect github.com/charmbracelet/x/conpty v0.1.0 // indirect @@ -97,7 +97,7 @@ require ( github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/net v0.35.0 // indirect + golang.org/x/net v0.36.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect diff --git a/go.sum b/go.sum index 6655db7d3b67c61670ef5680a56bab373d7884bb..91a8baea2b45cb5fff2f52414d7b68c5819af1d7 100644 --- a/go.sum +++ b/go.sum @@ -38,14 +38,14 @@ github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92 h github.com/charmbracelet/git-lfs-transfer v0.1.1-0.20240708204110-bacbfdb68d92/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= github.com/charmbracelet/glamour v0.9.1 h1:Q7PdJLOx8EoepsXUvW6Puz5WQ3YUElIGQdYKrIpiGLA= github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk= -github.com/charmbracelet/keygen v0.5.1 h1:zBkkYPtmKDVTw+cwUyY6ZwGDhRxXkEp0Oxs9sqMLqxI= -github.com/charmbracelet/keygen v0.5.1/go.mod h1:zznJVmK/GWB6dAtjluqn2qsttiCBhA5MZSiwb80fcHw= +github.com/charmbracelet/keygen v0.5.3 h1:2MSDC62OUbDy6VmjIE2jM24LuXUvKywLCmaJDmr/Z/4= +github.com/charmbracelet/keygen v0.5.3/go.mod h1:TcpNoMAO5GSmhx3SgcEMqCrtn8BahKhB8AlwnLjRUpk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250313133401-58331b1fff40 h1:9IxBdCOOJoJ1PYXgxWXJCk3Fkl7h2n+b7VtY+5BbIuQ= github.com/charmbracelet/lipgloss/v2 v2.0.0-alpha.2.0.20250313133401-58331b1fff40/go.mod h1:XhU7tcZRWVGzkjWQ6XYRH7tIVqYuWLx6XLjVqAz+7FU= -github.com/charmbracelet/log v0.4.1-0.20241101171137-e66b83d34ed9 h1:m72jc7WwobapT4NLVVAjejNsz26f2UQ2Mz74uTq/Tro= -github.com/charmbracelet/log v0.4.1-0.20241101171137-e66b83d34ed9/go.mod h1:soIjG88SDQxYFpbhYXbrDTPbd/07bfo66OjADdnY5HE= +github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk= +github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM= github.com/charmbracelet/ssh v0.0.0-20250128164007-98fd5ae11894 h1:Ffon9TbltLGBsT6XE//YvNuu4OAaThXioqalhH11xEw= @@ -96,8 +96,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -226,8 +226,8 @@ go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwE golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -239,8 +239,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/pkg/config/config.go b/pkg/config/config.go index 8db84ebf2763ad9fe15f24948c3268de39738d73..05dd2c38e3e3f70d6e7facfe526afd8fe0fc4215 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -61,15 +61,6 @@ type GitConfig struct { MaxConnections int `env:"MAX_CONNECTIONS" yaml:"max_connections"` } -// CORSConfig is the CORS configuration for the server. -type CORSConfig struct { - AllowedHeaders []string `env:"ALLOWED_HEADERS" yaml:"allowed_headers"` - - AllowedOrigins []string `env:"ALLOWED_ORIGINS" yaml:"allowed_origins"` - - AllowedMethods []string `env:"ALLOWED_METHODS" yaml:"allowed_methods"` -} - // HTTPConfig is the HTTP configuration for the server. type HTTPConfig struct { // Enabled toggles the HTTP server on/off @@ -86,9 +77,6 @@ type HTTPConfig struct { // PublicURL is the public URL of the HTTP server. PublicURL string `env:"PUBLIC_URL" yaml:"public_url"` - - // HTTP is the configuration for the HTTP server. - CORS CORSConfig `envPrefix:"CORS_" yaml:"cors"` } // StatsConfig is the configuration for the stats server. @@ -208,9 +196,6 @@ func (c *Config) Environ() []string { fmt.Sprintf("SOFT_SERVE_HTTP_TLS_KEY_PATH=%s", c.HTTP.TLSKeyPath), fmt.Sprintf("SOFT_SERVE_HTTP_TLS_CERT_PATH=%s", c.HTTP.TLSCertPath), fmt.Sprintf("SOFT_SERVE_HTTP_PUBLIC_URL=%s", c.HTTP.PublicURL), - fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS=%s", strings.Join(c.HTTP.CORS.AllowedHeaders, ",")), - fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS=%s", strings.Join(c.HTTP.CORS.AllowedOrigins, ",")), - fmt.Sprintf("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS=%s", strings.Join(c.HTTP.CORS.AllowedMethods, ",")), fmt.Sprintf("SOFT_SERVE_STATS_ENABLED=%t", c.Stats.Enabled), fmt.Sprintf("SOFT_SERVE_STATS_LISTEN_ADDR=%s", c.Stats.ListenAddr), fmt.Sprintf("SOFT_SERVE_LOG_FORMAT=%s", c.Log.Format), diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index b0653f28241239b14c225f76f247c154d67c82aa..8b84ed8222ba39ca5ad4d1236f5c787b19b0ab00 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -79,47 +79,3 @@ func TestCustomConfigLocation(t *testing.T) { cfg = DefaultConfig() is.Equal(cfg.Name, "Soft Serve") } - -func TestParseMultipleHeaders(t *testing.T) { - is := is.New(t) - is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS", "Accept,Accept-Language,User-Agent")) - t.Cleanup(func() { - is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS")) - }) - cfg := DefaultConfig() - is.NoErr(cfg.ParseEnv()) - is.Equal(cfg.HTTP.CORS.AllowedHeaders, []string{ - "Accept", - "Accept-Language", - "User-Agent", - }) -} - -func TestParseMultipleOrigins(t *testing.T) { - is := is.New(t) - is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS", "https://foo.example,https://foo.example2")) - t.Cleanup(func() { - is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS")) - }) - cfg := DefaultConfig() - is.NoErr(cfg.ParseEnv()) - is.Equal(cfg.HTTP.CORS.AllowedOrigins, []string{ - "https://foo.example", - "https://foo.example2", - }) -} - -func TestParseMultipleMethods(t *testing.T) { - is := is.New(t) - is.NoErr(os.Setenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS", "GET,POST,PUT")) - t.Cleanup(func() { - is.NoErr(os.Unsetenv("SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS")) - }) - cfg := DefaultConfig() - is.NoErr(cfg.ParseEnv()) - is.Equal(cfg.HTTP.CORS.AllowedMethods, []string{ - "GET", - "POST", - "PUT", - }) -} diff --git a/pkg/web/server.go b/pkg/web/server.go index ab336e89a5f358be803812b93144a10722af4010..74a04f5b176bee7d1710f023643436119b181c97 100644 --- a/pkg/web/server.go +++ b/pkg/web/server.go @@ -5,7 +5,6 @@ import ( "net/http" "github.com/charmbracelet/log" - "github.com/charmbracelet/soft-serve/pkg/config" "github.com/gorilla/handlers" "github.com/gorilla/mux" ) @@ -27,12 +26,5 @@ func NewRouter(ctx context.Context) http.Handler { h = handlers.CompressHandler(h) h = handlers.RecoveryHandler()(h) - cfg := config.FromContext(ctx) - - h = handlers.CORS(handlers.AllowedHeaders(cfg.HTTP.CORS.AllowedHeaders), - handlers.AllowedOrigins(cfg.HTTP.CORS.AllowedOrigins), - handlers.AllowedMethods(cfg.HTTP.CORS.AllowedMethods), - )(h) - return h } diff --git a/testscript/testdata/http-cors.txtar b/testscript/testdata/http-cors.txtar deleted file mode 100644 index c545ab7a1d908fde301410b90a651d8992aeb9e0..0000000000000000000000000000000000000000 --- a/testscript/testdata/http-cors.txtar +++ /dev/null @@ -1,64 +0,0 @@ -# vi: set ft=conf - -# FIXME: don't skip windows -[windows] skip 'curl makes github actions hang' - -# convert crlf to lf on windows -[windows] dos2unix http1.txt http2.txt http3.txt goget.txt gitclone.txt - -# start soft serve -exec soft serve & -# wait for SSH server to start -ensureserverrunning SSH_PORT - -# create user -soft user create user1 --key "$USER1_AUTHORIZED_KEY" - -# create access token -soft token create --expires-in '1h' 'repo2' -cp stdout tokenfile -envfile TOKEN=tokenfile -soft token create --expires-in '1ns' 'repo2' -cp stdout etokenfile -envfile ETOKEN=etokenfile -usoft token create 'repo2' -cp stdout utokenfile -envfile UTOKEN=utokenfile - -# push & create repo with some files, commits, tags... -mkdir ./repo2 -git -c init.defaultBranch=master -C repo2 init -mkfile ./repo2/README.md '# Project\nfoo' -mkfile ./repo2/foo.png 'foo' -mkfile ./repo2/bar.png 'bar' -git -C repo2 remote add origin http://$TOKEN@localhost:$HTTP_PORT/repo2 -git -C repo2 lfs install --local -git -C repo2 lfs track '*.png' -git -C repo2 add -A -git -C repo2 commit -m 'first' -git -C repo2 tag v0.1.0 -git -C repo2 push origin HEAD -git -C repo2 push origin HEAD --tags - -curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET' -stderr '.*Method Not Allowed.*' - -# stop the server -stopserver - -# allow cross-origin OPTIONS requests -env SOFT_SERVE_HTTP_CORS_ALLOWED_ORIGINS="https://foo.example" -env SOFT_SERVE_HTTP_CORS_ALLOWED_METHODS="GET,OPTIONS" -env SOFT_SERVE_HTTP_CORS_ALLOWED_HEADERS="Origin,Access-Control-Request-Method" - -# restart soft serve -exec soft serve & -# wait for SSH server to start -ensureserverrunning SSH_PORT - -curl -v --request OPTIONS http://localhost:$HTTP_PORT/repo2.git/info/refs -H 'Origin: https://foo.example' -H 'Access-Control-Request-Method: GET' -stderr '.*200 OK.*' - -# stop the server -[windows] stopserver -[windows] ! stderr . diff --git a/testscript/testdata/repo-push.txtar b/testscript/testdata/repo-push.txtar index d9f947537f105e879ab54275777c62250ffaa673..c4a5e243e1a9b021d207dddb6279588beeab37fe 100644 --- a/testscript/testdata/repo-push.txtar +++ b/testscript/testdata/repo-push.txtar @@ -11,8 +11,14 @@ soft repo create repo-empty -d 'description' -H -p -n 'repo-empty' # clone repo git clone ssh://localhost:$SSH_PORT/repo-empty repo-empty -# push repo +# push repo without any commits ! git -C repo-empty push origin HEAD +# push repo with a commit +mkfile ./repo-empty/README.md '# Hello\n\nwelcome' +git -C repo-empty add README.md +git -C repo-empty commit -m 'first' +git -C repo-empty push origin HEAD + # stop the server [windows] stopserver