11

In Ruby/Rack, I'm able to get the scheme of the current request URL from scheme#request. However, in Go, http.Request.URL.Scheme returns an empty string:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%#v\n", r.URL.Scheme) // Always shows empty string
}

How do I get scheme of the current request URL?

Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
Ethan
  • 18,584
  • 15
  • 51
  • 72
  • you may inspect `r.Proto` it will return HTTP/1.1 or HTTP/2 according with the documentation, but not sure if it will change for HTTPS – Yandry Pozo Nov 27 '16 at 07:52
  • @YandryPozo `r.Proto` will only be `HTTP/2` if the Go application is terminating TLS itself, i.e. with `http.ListenAndServeTLS()`. – Michael Hampton Mar 16 '18 at 03:37
  • 2
    Since you use ListenAndServe and not ListenAndServeTLS, your scheme can be safely assumed as http. If you use both tls and non tls versions, you can use r.TLS and check it for null to know whether TLS was established. If your go app is running behind a reverse proxy, there's virtually no way to know what the scheme was. – Michael Kruglos Oct 17 '18 at 10:47
  • @MichaelKruglos your answer really is the only REAL valid answer here, considering many apps are now running behind webservices (like nginx) that may, or may not terminate TLS for the app, in a given environment. – RavenHursT Oct 10 '19 at 20:50

6 Answers6

11

To serve HTTP and HTTPS you will need to call both serve functions
http.ListenAndServe() and http.ListenAndServeTLS() with the same handler because if you are using only 1 of them like the example of the question then you are only listing on 1 protocol http.ListenAndServe() for http, and http.ListenAndServeTLS() for HTTPS, if you will try to contact the server with a different protocol it will not go through,

and because HTTPS is HTTP over TLS the *http.Request has a TLS property that will give you back a *tls.ConnectionState with info about the TLS that was used on this request, then if you want to know how the client contact the server you can check on the request TLS property, if the request were made with HTTPS it would not be nil, if the request was made with HTTP then the TLS property will be nil, because the only way that a request was made with TLS is with the HTTPS protocol

func handler(w http.ResponseWriter, r *http.Request) {
   if r.TLS == nil {
       // the scheme was HTTP
   } else {
       // the scheme was HTTPS
   }                 
}  

func main() {
    http.HandleFunc("/", handler)
    go func(){ 
        log.Fatal(http.ListenAndServeTLS(":8443","localhost.crt", "localhost.key", nil)) 
    }()
    log.Fatal(http.ListenAndServe(":8080", nil))
}                                                   

Isaac Weingarten
  • 977
  • 10
  • 15
  • 1
    While this code may provide a solution to the question, it's better to add context as to why/how it works. This can help future users learn, and apply that knowledge to their own code. You are also likely to have positive feedback from users in the form of upvotes, when the code is explained. – borchvm Apr 27 '20 at 09:31
6

A quick grep shows that r.URL.Scheme is never set to anything other than the empty string anywhere in net/http. Personally I think it should be, as far as possible, but apparently I have a minority opinion.


If you opened a TLS listener yourself with http.ListenAndServeTLS() then presumably you know the scheme is https already. You can use a trivial middleware handler that fills in r.URL.Scheme in this case.

func AlwaysHTTPS(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        r.URL.Scheme = "https"

        next.ServeHTTP(w, r)
    })
}

If you're running behind a web server, then it may pass the request protocol in a header such as X-Forwarded-Proto. In this case, you can use a handler like gorilla's handlers.ProxyHeaders() to fill in the missing fields.

An example using gorilla mux:

package main

import (
    "log"
    "net/http"

    "github.com/gorilla/handlers"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.Use(handlers.ProxyHeaders)

    http.Handle("/", r)
    log.Fatal(http.ListenAndServe("[::]:8009", nil))
}

From its comments:

ProxyHeaders inspects common reverse proxy headers and sets the corresponding fields in the HTTP request struct. These are X-Forwarded-For and X-Real-IP for the remote (client) IP address, X-Forwarded-Proto or X-Forwarded-Scheme for the scheme (http|https) and the RFC7239 Forwarded header, which may include both client IPs and schemes.

NOTE: This middleware should only be used when behind a reverse proxy like nginx, HAProxy or Apache. Reverse proxies that don't (or are configured not to) strip these headers from client requests, or where these headers are accepted "as is" from a remote client (e.g. when Go is not behind a proxy), can manifest as a vulnerability if your application uses these headers for validating the 'trustworthiness' of a request.

Community
  • 1
  • 1
Michael Hampton
  • 9,737
  • 4
  • 55
  • 96
5

Since you use ListenAndServe and not ListenAndServeTLS, your scheme can be safely assumed as http. If you use both tls and non tls versions, you can use r.TLS and check it for null to know whether TLS was established. If your go app is running behind a reverse proxy, then you have to check the documentation on the web server which is forwarding the requests to your app, to learn how to configure it to pass this information as headers. Here's a link describing nginx configuration which accomplishes that. You can easily find configuration guides for other webservers as well.

Better yet, configure HSTS on your main web server, so that you don't have to worry about insecure connections altogether. There are very few, if any, legitimate uses to non-TLS http. For nginx you can find this article useful. And again for other web servers you will easily find configuration guides.

If you're unsure if your site/application needs https I recommend reading this.

Michael Kruglos
  • 1,205
  • 14
  • 26
1

localhost is a special case for URL formation. It is going to be empty anyway if your client is localhost.

net.http package doc:

As a special case, if req.URL.Host is "localhost" (with or without a port number), then a nil URL and nil error will be returned.

The way to get required url/uri information is to get it from http.Request directly. For example:

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s\n", r.Host)                    
}                                                     
I159
  • 29,741
  • 31
  • 97
  • 132
1

http header X-Forwarded-Proto will have http or https

  • 1
    While that is true, it's not a browser-header, it is a proxy-header, and will only be present when passing through a proxy or load-balancer if the defaults are selected for config. One does not need to use the header, it is optional. – jnovack Oct 12 '20 at 09:35
-2

It is because, you're accessing the HTTP server so:

GET / HTTP/1.1
Host: localhost:8080

in this cases, based on parsing what you get is raw URL from Go's http.Request.URL. why you are getting this is because you are accessing the URL from a relative path, hence the lack of a Host or Scheme in the URL object.

If you do want to get the HTTP host, you may have to access the Host attribute of the http.Request struct. See http://golang.org/pkg/http/#Request

as it is not directly available, but you can still be able to assemble it:

u := r.URL

// The scheme can be http/https because that's depends on protocol your server handles.
u.Scheme = "http"
Brij
  • 259
  • 2
  • 8
  • 1
    http://stackoverflow.com/questions/6899069/why-are-request-url-host-and-scheme-blank-in-the-development-server Just copied from the older answer. – I159 Nov 27 '16 at 10:43