├── .travis.yml ├── README.md ├── go.mod ├── go.sum └── httpsify.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | env: 4 | matrix: 5 | - _GOOS=windows _GOARCH=386 6 | - _GOOS=windows _GOARCH=amd64 7 | - _GOOS=linux _GOARCH=386 8 | - _GOOS=linux _GOARCH=amd64 9 | - _GOOS=linux _GOARCH=arm 10 | - _GOOS=darwin _GOARCH=amd64 11 | 12 | script: 13 | - export GOOS=${_GOOS} 14 | - export GOARCH=${_GOARCH} 15 | - '[ "${GOOS}" = "linux" ] && export CGO_ENABLED=0 || true' 16 | - go get -v 17 | - go build -v 18 | 19 | before_deploy: 20 | - zip httpsify-${GOOS}-${GOARCH}.zip httpsify{,.exe} 21 | 22 | deploy: 23 | provider: releases 24 | api_key: 25 | secure: jH8TGNkOe8lVLl/H1mvBZRaN5Hi1iRotqtVOa6Gs4MPUaRwIYnJB3XsxeX+M5or1nWXIpJQkhxRCpcWGdzCmI1SREkfOYQB7EK7XVsqUYkQgwAE5ZYdEZD/oj12Anav42zErbibJYZTJ1bFOYHYJZoaM2wSoWKPvw2aIQvOMJVFlHAdtvp8QYZ7j+qAjRP4kXGZk40SO4cySCo1kADjpUAQS8MLK45YOc6ybe6/X464dlYkdRcABAng+dALbSnyR6YXn2CFsXNfRXRTmt64DtEibvKibeJnfLL2wZf0Xf1zGs1KnpbiSY6Sy3pHprxUStSh9IVS8I13ssS7GBMicsEVhs+yC4nwReggwB0riNDrYuw81zwWYuOHeL7zthhtpTuBiypGG6QYF6hS2jNfXV9lLtcP0O55CZyC+jDUoX029Irp5l077teTGarJJSqzGOokS193p6XUHUvdaYJp1HSGM0nfkMn8JBkk9I5b/WVMEIl1gdJ+dacsBj3OtE8/X8ioQXdMR4L5lsnGT7qBK0vanLV715bnCc9OTexRaiCMBdgu2KYI9gu/KsIQvjl/jZGgWE3c5f/RlWdw/+WHqw+0dKYOnln1OMDU6r+Dpaz200ArxK5UQgNyZDaaBrghT9DP13ds1WWzcwA1Zq6hIlV8XUdXMoz+T7GxcL9ESJpA= 26 | file_glob: true 27 | skip_cleanup: true 28 | file: httpsify-*.zip 29 | on: 30 | repo: scottjg/httpsify 31 | tags: true 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | A transparent HTTPS proxy with automatic certificate renewal 3 | using https://letsencrypt.org/ 4 | 5 | # How it works ? 6 | httpsify is a https reverse proxy ... 7 | [https request] --> httpsify --> [apache/nginx/nodejs/... etc] 8 | but this isn't the point because there are many https offloaders, 9 | but httpsify uses letsencrypt (https://letsencrypt.org/) 10 | for automatically generating free and valid ssl certificates, as well as auto renewal of certs, 11 | this web server by default uses HTTP/2 . 12 | you can say that httpsify is just a http/2 & letsencrypt wrapper for any http web server with no hassle, it just works . 13 | 14 | # Features 15 | * SSL Offloader. 16 | * HTTP/2 support. 17 | * Multi-Core support. 18 | * Auto-Renewal for generated certificates. 19 | * Blazing fast. 20 | * Very light. 21 | * Portable and small `~ 2 MB` 22 | * No system requirements. 23 | * No configurations required, just `httpsify --domains="domain.com,www.domain.com,sub.domain.com"` 24 | * Passes `X-Forwarded-*` headers, `X-Real-IP` header and `X-Remote-IP`/`X-Remote-Port` to the backend server. 25 | 26 | # Installation 27 | > Currently the only available binaries are built for `linux` `386/amd64` and you can download them from [here](https://github.com/alash3al/httpsify/releases) . 28 | 29 | # Building from source : 30 | * Make sure you have `Golang` installed . 31 | * `go get github.com/alash3al/httpsify`. 32 | * `go install github.com/alash3al/httpsify`. 33 | * make sure that `$GOPATH/bin` in your `$PATH` . 34 | 35 | # Quick Usage 36 | > lets say that you have extracted/built httpsify in the current working directory . 37 | ```bash 38 | # this is the simplest way to run httpsify 39 | # this will run a httpsify instance listening on port 443 and passing the incoming requests to http://localhost 40 | # and building valid signed cerificates for the specified domains [they must be valid domain names] 41 | ./httpsify --domains="domain.tld,www.domain.tld,another.domain.tld" 42 | ``` 43 | 44 | # Author 45 | I'm [Mohammed Al Ashaal](https://www.alash3al.xyz) 46 | 47 | # Thanks 48 | I must thank the following awesome libraries 49 | 50 | * [github.com/xenolf/lego](https://github.com/xenolf/lego) 51 | * [github.com/dkumor/acmewrapper](https://github.com/dkumor/acmewrapper) 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scottjg/httpsify 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/dkumor/acmewrapper v1.0.0 7 | github.com/scottjg/go-nat v0.0.0-20161221075106-5c8c1ab98c62 8 | ) 9 | 10 | require ( 11 | github.com/jackpal/gateway v1.0.7 // indirect 12 | github.com/jackpal/go-nat-pmp v1.0.2 // indirect 13 | github.com/miekg/dns v1.0.15 // indirect 14 | github.com/scottjg/upnp v0.0.0-20161226025956-82caf20da2dd // indirect 15 | github.com/stretchr/testify v1.8.1 // indirect 16 | github.com/xenolf/lego v0.4.1 // indirect 17 | golang.org/x/crypto v0.5.0 // indirect 18 | golang.org/x/net v0.5.0 // indirect 19 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 20 | golang.org/x/sys v0.4.0 // indirect 21 | gopkg.in/square/go-jose.v1 v1.1.2 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/dkumor/acmewrapper v1.0.0 h1:ada878JLe3LmlHB2ApYDBpajTlrgEc+m10xdUyduKLA= 5 | github.com/dkumor/acmewrapper v1.0.0/go.mod h1:ijQrxguqLi3T4MSOcLmCDdbh8nKkB1FwhlNLrSYJ9qE= 6 | github.com/jackpal/gateway v1.0.7 h1:7tIFeCGmpyrMx9qvT0EgYUi7cxVW48a0mMvnIL17bPM= 7 | github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsSd1AcIbA= 8 | github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 9 | github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 10 | github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw= 11 | github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 12 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 13 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 14 | github.com/scottjg/go-nat v0.0.0-20161221075106-5c8c1ab98c62 h1:wkpcmmTD5cLqnOJ9vzRKl3JIk6gX47GNIfGF+RzXjfU= 15 | github.com/scottjg/go-nat v0.0.0-20161221075106-5c8c1ab98c62/go.mod h1:iFL7kOSQeODLMCxse4StDu2KlmLFaGGzaa2UQe4PdKg= 16 | github.com/scottjg/upnp v0.0.0-20161226025956-82caf20da2dd h1:kcen6KQmsoz9ZiOjxfR1uoDwBbBV4dhH1yEu0k5LMZU= 17 | github.com/scottjg/upnp v0.0.0-20161226025956-82caf20da2dd/go.mod h1:l168au4IoTMJEcotw+BRMFTQDyLfnBD/KyUwJbPTMWs= 18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 19 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 20 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 21 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 23 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 24 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 25 | github.com/xenolf/lego v0.4.1 h1:D/QMnzmGNhYMIqifwt1OMoSeERrClOPuXDP4HrC3ZPY= 26 | github.com/xenolf/lego v0.4.1/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY= 27 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 28 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 29 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 30 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 31 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 32 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 34 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/square/go-jose.v1 v1.1.2 h1:/5jmADZB+RiKtZGr4HxsEFOEfbfsjTKsVnqpThUpE30= 37 | gopkg.in/square/go-jose.v1 v1.1.2/go.mod h1:QpYS+a4WhS+DTlyQIi6Ka7MS3SuR9a055rgXNEe6EiA= 38 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /httpsify.go: -------------------------------------------------------------------------------- 1 | // httpsify is a transparent blazing fast https offloader with auto certificates renewal . 2 | // this software is published under MIT License . 3 | // by Mohammed Al ashaal with the help of those opensource libraries [github.com/xenolf/lego, github.com/dkumor/acmewrapper] . 4 | package main 5 | 6 | import ( 7 | "crypto/tls" 8 | "flag" 9 | "fmt" 10 | "github.com/dkumor/acmewrapper" 11 | "log" 12 | "net" 13 | "net/http" 14 | "net/http/httputil" 15 | "net/url" 16 | "os" 17 | "os/signal" 18 | "path/filepath" 19 | "strconv" 20 | "strings" 21 | "time" 22 | 23 | "github.com/scottjg/go-nat" 24 | ) 25 | 26 | // -------------- 27 | 28 | const version = "httpsify/holepuncher/v2" 29 | 30 | var ( 31 | port = flag.String("port", "4443", "the port that will serve the https requests") 32 | ddns = flag.String("ddns", "", "specify provider (e.g. namecheap or iwantmyname) or update url") 33 | cert = flag.String("cert", "./cert.pem", "the cert.pem save-path") 34 | key = flag.String("key", "./key.pem", "the key.pem save-path") 35 | backend = flag.String("backend", "http://127.0.0.1:80", "the backend http server that will serve the terminated requests") 36 | info = flag.String("info", "yes", "whether to send information about httpsify or not ^_^") 37 | skipnatfwd = flag.Bool("skipnatfwd", false, "don't automatically setup a port forwarding rule on the upstream NAT router") 38 | natfwd = flag.String("natfwd", "default", "comma separated list of internal:external ports to map, defaults to opening port 443 to point to your https server") 39 | ) 40 | 41 | // -------------- 42 | 43 | var domain = "" 44 | var portsToMap = map[int]int{} 45 | 46 | func init() { 47 | flag.Parse() 48 | args := flag.Args() 49 | if len(args) < 1 { 50 | domain = "" 51 | } else { 52 | domain = args[0] 53 | } 54 | 55 | _, err := strconv.Atoi(*port) 56 | if err != nil { 57 | log.Fatalf("bogus port: %s", err) 58 | } 59 | 60 | if *skipnatfwd { 61 | return 62 | } 63 | 64 | if *natfwd == "default" { 65 | *natfwd = fmt.Sprintf("%s:443", *port) 66 | } 67 | 68 | for _, portRange := range strings.Split(*natfwd, ",") { 69 | p := strings.Split(portRange, ":") 70 | intPort, err := strconv.Atoi(p[0]) 71 | if err != nil { 72 | log.Fatalf("bogus natfwd parameter: %v", err) 73 | return 74 | } 75 | extPort, err := strconv.Atoi(p[1]) 76 | if err != nil { 77 | log.Fatalf("bogus natfwd parameter: %v", err) 78 | return 79 | } 80 | portsToMap[intPort] = extPort 81 | } 82 | } 83 | 84 | // -------------- 85 | 86 | func main() { 87 | backendUrl, err := url.Parse(*backend) 88 | if err != nil { 89 | log.Fatalf("bogus backend url: %s", err) 90 | } 91 | 92 | if len(portsToMap) > 0 { 93 | gw, err := nat.DiscoverGateway() 94 | if err != nil { 95 | log.Fatalf("error: %s", err) 96 | } 97 | log.Printf("Detected gateway type: %v\n", gw.Type()) 98 | for intPort, extPort := range portsToMap { 99 | gw.DeletePortMapping("tcp", intPort, extPort) 100 | err = gw.AddPortMapping("tcp", intPort, extPort, "httpsify", 60*time.Second) 101 | if err != nil { 102 | log.Fatalf("error: %s", err) 103 | } 104 | log.Printf("Mapped internal port %v to external port %v.\n", intPort, extPort) 105 | } 106 | 107 | // unmap the port if you ctrl-C or if we finish running main(). 108 | // in other situations, we may leak the mapping, but it will expire 109 | // after a minute, so it could be worse. 110 | c := make(chan os.Signal, 1) 111 | signal.Notify(c, os.Interrupt) 112 | go func() { 113 | for _ = range c { 114 | for intPort, extPort := range portsToMap { 115 | gw.DeletePortMapping("tcp", intPort, extPort) 116 | } 117 | os.Exit(1) 118 | } 119 | }() 120 | 121 | go func() { 122 | for { 123 | time.Sleep(30 * time.Second) 124 | for intPort, extPort := range portsToMap { 125 | err = gw.AddPortMapping("tcp", intPort, extPort, "httpsify", 60*time.Second) 126 | if err != nil { 127 | log.Printf("error: %s\n", err) 128 | } 129 | } 130 | } 131 | }() 132 | } 133 | 134 | if *ddns != "" { 135 | updateUrl := "" 136 | dnsUsername := os.Getenv("DNS_USERNAME") 137 | dnsPassword := os.Getenv("DNS_PASSWORD") 138 | 139 | var req *http.Request 140 | switch *ddns { 141 | case "namecheap": 142 | if dnsPassword == "" { 143 | log.Fatalf("Need to define DNS_PASSWORD in your environment") 144 | } 145 | 146 | domainLevels := strings.Split(domain, ".") 147 | host := "@" 148 | sld := domain 149 | if len(domainLevels) > 2 { 150 | host = strings.Join(domainLevels[0:(len(domainLevels)-2)], ".") 151 | sld = strings.Join(domainLevels[(len(domainLevels)-2):len(domainLevels)], ".") 152 | } 153 | updateUrl = "https://dynamicdns.park-your-domain.com/update?host=" + host + "&domain=" + sld + "&password=" + dnsPassword 154 | req, err = http.NewRequest("GET", updateUrl, nil) 155 | break 156 | case "iwantmyname": 157 | if dnsPassword == "" || dnsUsername == "" { 158 | log.Fatalf("Need to define DNS_USERNAME and DNS_PASSWORD in your environment") 159 | } 160 | updateUrl = "https://iwantmyname.com/basicauth/ddns?hostname=" + domain 161 | req, err = http.NewRequest("GET", updateUrl, nil) 162 | if req != nil { 163 | req.SetBasicAuth(dnsUsername, dnsPassword) 164 | } 165 | break 166 | 167 | default: 168 | if dnsPassword == "" || dnsUsername == "" { 169 | log.Print("Warning: ddns url specified without username/password") 170 | } 171 | 172 | req, err = http.NewRequest("GET", updateUrl, nil) 173 | if req != nil && dnsPassword != "" && dnsUsername != "" { 174 | req.SetBasicAuth(dnsUsername, dnsPassword) 175 | } 176 | } 177 | 178 | if err != nil { 179 | log.Fatalf("ddns error: %v", err) 180 | } 181 | client := &http.Client{} 182 | resp, err := client.Do(req) 183 | if err != nil { 184 | log.Fatalf("ddns error: %v", err) 185 | } 186 | 187 | if resp.StatusCode != 200 { 188 | log.Fatalf("ddns error: got http status code %v from api", resp.StatusCode) 189 | } 190 | 191 | go func() { 192 | for { 193 | time.Sleep(60 * time.Second) 194 | resp, err := client.Do(req) 195 | if err != nil { 196 | log.Println("ddns error: %v", err) 197 | } 198 | 199 | if resp.StatusCode != 200 { 200 | log.Println("ddns error: got http status code %v from api", resp.StatusCode) 201 | } 202 | } 203 | }() 204 | } 205 | 206 | if domain != "" { 207 | acme, err := acmewrapper.New(acmewrapper.Config{ 208 | Domains: []string{domain}, 209 | Address: ":" + *port, 210 | TLSCertFile: *cert, 211 | TLSKeyFile: *key, 212 | RegistrationFile: filepath.Dir(*cert) + "/lets-encrypt-user.reg", 213 | PrivateKeyFile: filepath.Dir(*cert) + "/lets-encrypt-user.pem", 214 | TOSCallback: acmewrapper.TOSAgree, 215 | }) 216 | if err != nil { 217 | log.Fatal("err> " + err.Error()) 218 | } 219 | listener, err := tls.Listen("tcp", ":"+*port, acme.TLSConfig()) 220 | if err != nil { 221 | log.Fatal("err> " + err.Error()) 222 | } 223 | 224 | reverseProxy := httputil.NewSingleHostReverseProxy(backendUrl) 225 | log.Fatal(http.Serve(listener, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 226 | uip, uport, _ := net.SplitHostPort(r.RemoteAddr) 227 | 228 | r.Host = r.Host 229 | r.Header.Set("Host", r.Host) 230 | r.Header.Set("X-Real-IP", uip) 231 | r.Header.Set("X-Remote-IP", uip) 232 | r.Header.Set("X-Remote-Port", uport) 233 | r.Header.Set("X-Forwarded-For", uip) 234 | r.Header.Set("X-Forwarded-Proto", "https") 235 | r.Header.Set("X-Forwarded-Host", r.Host) 236 | r.Header.Set("X-Forwarded-Port", *port) 237 | reverseProxy.ServeHTTP(w, r) 238 | }))) 239 | } 240 | select {} 241 | } 242 | --------------------------------------------------------------------------------