Skip to content

Commit

Permalink
integrate let's encrypt support
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhener committed Apr 30, 2024
1 parent b32ef23 commit c8e2a5c
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 58 deletions.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ goshs is a replacement for Python's `SimpleHTTPServer`. It allows uploading and
* Basic Authentication
* Transport Layer Security (HTTPS)
* self-signed
* let's encrypt
* provide own certificate
* Non persistent clipboard
* Download clipboard entries as .json file
Expand Down Expand Up @@ -83,10 +84,16 @@ Web server options:
-c, --cli Enable cli (only with auth and tls) (default: false)

TLS options:
-s, --ssl Use TLS
-ss, --self-signed Use a self-signed certificate
-sk, --server-key Path to server key
-sc, --server-cert Path to server certificate
-s, --ssl Use TLS
-ss, --self-signed Use a self-signed certificate
-sk, --server-key Path to server key
-sc, --server-cert Path to server certificate
-sl, --lets-encrypt Use Let's Encrypt as certification service
-sld, --le-domains Domain(s) to request from Let's Encrypt (comma separated list)
-sle, --le-email Email to use with Let's Encrypt
-slh, --le-http Port to use for Let's Encrypt HTTP Challenge (default: 80)
-slt, --le-tls Port to use for Let's Encrypt TLS ALPN Challenge (default: 443)
Authentication options:
-b, --basic-auth Use basic authentication (user:pass - user can be empty)
Expand All @@ -102,10 +109,11 @@ Usage examples:
Start with wevdav support: ./goshs -w
Start with different port: ./goshs -p 8080
Start with self-signed cert: ./goshs -s -ss
Start with let's encrypt: ./goshs -s -sl -sle [email protected] -sld your.domain.com,your.seconddomain.com
Start with custom cert: ./goshs -s -sk <path to key> -sc <path to cert>
Start with basic auth: ./goshs -b secret-user:$up3r$3cur3
Start with basic auth empty user: ./goshs -b :$up3r$3cur3
Start with cli enabled: ./goshs -b secret-user:$up3r$3cur3 -s -ss -c
Start with cli enabled: ./goshs -b secret-user:$up3r$3cur3 -s -ss -c
```

# Examples
Expand Down Expand Up @@ -138,6 +146,20 @@ Usage examples:

`goshs -s -ss`

*Let's encrypt*

`./goshs -s -sl -sle [email protected] -sld your.domain.com,your.seconddomain.com`

You will have to make sure that your IP is reachable via the domain name by creating an A entry with you DNS service provider first.

Then the example command will create two files called `key` and `cert` if the request for a certificate is successful. *Please note:* for this to work let's encrypt needs to reach goshs at port 80 and 443. So you will need to start it as root. There are several options you can choose from to circumvent running goshs as root after obtaining a valid certificate:

- Drop user privileges using `-u` (preferred)
- Run it once as root until you obtain the certificate. Then stop it and rerun it using `key` and `cert` like: `./goshs -s -sk key -sc cert` as non-root user
- Use `-slh` and `-slt` to choose different challenge ports and proxy port 80 and 443 to them

After stopping goshs you can reuse the files `key` and `cert` to restart the server with a valid certificate like: `./goshs -s -sk key -sc -cert` until they are invalidated due to certificate lifetime (90 days).

*Provide own certificate*

`goshs -s -sk server.key -sc server.crt`
Expand All @@ -150,6 +172,8 @@ This mode will omit the dir listing on the web interface. Also you will not have
**Retrieve the directory listing in json format**
You can now retrieve the directory listing in *json* format. This is meant to be used with curl for example in environments where you do not have a browser on hand.



```bash
curl -s localhost:8000/?json | jq
[
Expand Down
142 changes: 142 additions & 0 deletions ca/letsencrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package ca

import (
"bufio"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"os"
"strings"

"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/certificate"
"github.com/go-acme/lego/v4/challenge/http01"
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
"github.com/patrickhener/goshs/logger"
)

// LetsEncryptUser is the struct of all information needed to rollout a lets encrypt certificate
type LetsEncryptUser struct {
Email string
Registration *registration.Resource
Key crypto.PrivateKey
HTTPPort string
TLSPort string
Domains []string
Config *lego.Config
Client *lego.Client
}

// GetEmail will return the Users Email
func (u *LetsEncryptUser) GetEmail() string {
return u.Email
}

// Get Registration will return the Registration
func (u *LetsEncryptUser) GetRegistration() *registration.Resource {
return u.Registration
}

// GetPrivateKey will return the Private Key
func (u *LetsEncryptUser) GetPrivateKey() crypto.PrivateKey {
return u.Key
}

func (u *LetsEncryptUser) RequestCertificate() ([]byte, []byte) {
// Create a user. New accounts need an email and private key to start.
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
logger.Fatalf("error generating private key for lets encrypt: %+v", err)
}

u.Key = privateKey

u.Config = lego.NewConfig(u)

// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
u.Config.Certificate.KeyType = certcrypto.RSA2048

// A client facilitates communication with the CA server.
u.Client, err = lego.NewClient(u.Config)
if err != nil {
logger.Fatalf("error retrieving client for communication with lets encrypt acme server: %+v", err)
}

err = u.Client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", u.TLSPort))
if err != nil {
logger.Fatalf("error setting tls alpn provider for lets encrypt: %+v", err)
}

err = u.Client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", u.HTTPPort))
if err != nil {
logger.Fatalf("error setting http provider for lets encrypt: %+v", err)
}

// New users will need to register
reg, err := u.Client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
logger.Fatalf("error registering new user with lets encrypt: %+v", err)
}
u.Registration = reg

request := certificate.ObtainRequest{
Domains: u.Domains,
Bundle: true,
}

certificates, err := u.Client.Certificate.Obtain(request)
if err != nil {
logger.Fatalf("error registering domain(s) with lets encrypt: %+v", err)
}

return certificates.PrivateKey, certificates.Certificate
}

func GetLECertificateAndKey(email string, domains []string, httpPort string, tlsPort string) {
// Get email if none provided
if email == "" {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Provide E-Mail to use with Let's Encrypt: ")
resultEmail, err := reader.ReadString('\n')
if err != nil {
logger.Fatalf("error reading email from stdin: %+v", err)
}
email = strings.Trim(resultEmail, "\n")
}

// Get domains if none are provided
if domains[0] == "" {
reader := bufio.NewReader(os.Stdin)
fmt.Print("Provide domain(s) to request with Let's Encrypt: ")
resultDomains, err := reader.ReadString('\n')
if err != nil {
logger.Fatalf("error reading domains from stdin: %+v", err)
}
resultDomains = strings.Trim(resultDomains, "\n")
domains = strings.Split(resultDomains, ",")
}

// construct letsencrypt user object
letsencryptUser := LetsEncryptUser{
Email: email,
HTTPPort: httpPort,
TLSPort: tlsPort,
Domains: domains,
}

key, cert := letsencryptUser.RequestCertificate()

err := os.WriteFile("key", key, 0644)
if err != nil {
logger.Fatalf("error writing file 'key': %+v", err)
}

err = os.WriteFile("cert", cert, 0644)
if err != nil {
logger.Fatalf("error writing file 'cert': %+v", err)
}
}
13 changes: 11 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
module github.com/patrickhener/goshs

go 1.19
go 1.21

toolchain go1.21.8

require (
github.com/go-acme/lego/v4 v4.16.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef
github.com/sirupsen/logrus v1.8.1
github.com/sirupsen/logrus v1.9.3
golang.org/x/crypto v0.21.0
golang.org/x/net v0.23.0
)

require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/miekg/dns v1.1.58 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect
)
35 changes: 30 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-acme/lego/v4 v4.16.1 h1:JxZ93s4KG0jL27rZ30UsIgxap6VGzKuREsSkkyzeoCQ=
github.com/go-acme/lego/v4 v4.16.1/go.mod h1:AVvwdPned/IWpD/ihHhMsKnveF7HHYAz/CmtXi7OZoE=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM=
github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
8 changes: 3 additions & 5 deletions httpserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package httpserver
import (
"crypto/tls"
"fmt"
"io/ioutil"
"io"
"log"
"net"
"net/http"
Expand Down Expand Up @@ -60,7 +60,7 @@ func (fs *FileServer) Start(what string) {
// Addr: addr,
Handler: http.AllowQuerySemicolons(mux),
ReadHeaderTimeout: 10 * time.Second, // Mitigate Slow Loris Attack
ErrorLog: log.New(ioutil.Discard, "", 0),
ErrorLog: log.New(io.Discard, "", 0),
// Against good practice no timeouts here, otherwise big files would be terminated when downloaded
}

Expand Down Expand Up @@ -102,7 +102,6 @@ func (fs *FileServer) Start(what string) {
fs.dropPrivs()

logger.Panic(server.ServeTLS(listener, "", ""))
// logger.Panic(server.ListenAndServeTLS("", ""))
} else {
if fs.MyCert == "" || fs.MyKey == "" {
logger.Fatal("You need to provide server.key and server.crt if -s and not -ss")
Expand Down Expand Up @@ -131,7 +130,6 @@ func (fs *FileServer) Start(what string) {
fs.dropPrivs()

logger.Panic(server.ServeTLS(listener, "", ""))
//logger.Panic(server.ListenAndServeTLS(fs.MyCert, fs.MyKey))
}
} else {
fs.logStart(what)
Expand All @@ -140,6 +138,6 @@ func (fs *FileServer) Start(what string) {
fs.dropPrivs()

logger.Panic(server.Serve(listener))
//logger.Panic(server.ListenAndServe())
}

}
1 change: 1 addition & 0 deletions httpserver/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type FileServer struct {
Webroot string
SSL bool
SelfSigned bool
LetsEncrypt bool
MyKey string
MyCert string
User string
Expand Down
Loading

0 comments on commit c8e2a5c

Please sign in to comment.