forked from TrueCloudLab/certificates
Read host and protocol information from request for links
When constructing links we want to read the required host and protocol information in a dynamic manner from the request for constructing ACME links such as the directory information. This way, if the server is running behind a proxy, and we don't know what the exposed URL should be at runtime, we can construct the required information from the host, tls and X-Forwarded-Proto fields in the HTTP request. Inspired by the LetsEncrypt Boulder project (web/relative.go).
This commit is contained in:
parent
f126962f3f
commit
639993bd09
7 changed files with 136 additions and 13 deletions
|
@ -123,7 +123,7 @@ func (h *Handler) GetDirectory(w http.ResponseWriter, r *http.Request) {
|
||||||
api.WriteError(w, err)
|
api.WriteError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dir := h.Auth.GetDirectory(prov)
|
dir := h.Auth.GetDirectory(prov, baseURLFromRequest(r))
|
||||||
api.JSON(w, dir)
|
api.JSON(w, dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ type mockAcmeAuthority struct {
|
||||||
getAuthz func(p provisioner.Interface, accID string, id string) (*acme.Authz, error)
|
getAuthz func(p provisioner.Interface, accID string, id string) (*acme.Authz, error)
|
||||||
getCertificate func(accID string, id string) ([]byte, error)
|
getCertificate func(accID string, id string) ([]byte, error)
|
||||||
getChallenge func(p provisioner.Interface, accID string, id string) (*acme.Challenge, error)
|
getChallenge func(p provisioner.Interface, accID string, id string) (*acme.Challenge, error)
|
||||||
getDirectory func(provisioner.Interface) *acme.Directory
|
getDirectory func(provisioner.Interface, string) *acme.Directory
|
||||||
getLink func(acme.Link, string, bool, ...string) string
|
getLink func(acme.Link, string, bool, ...string) string
|
||||||
getOrder func(p provisioner.Interface, accID string, id string) (*acme.Order, error)
|
getOrder func(p provisioner.Interface, accID string, id string) (*acme.Order, error)
|
||||||
getOrdersByAccount func(p provisioner.Interface, id string) ([]string, error)
|
getOrdersByAccount func(p provisioner.Interface, id string) ([]string, error)
|
||||||
|
@ -108,9 +108,9 @@ func (m *mockAcmeAuthority) GetChallenge(p provisioner.Interface, accID, id stri
|
||||||
return m.ret1.(*acme.Challenge), m.err
|
return m.ret1.(*acme.Challenge), m.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockAcmeAuthority) GetDirectory(p provisioner.Interface) *acme.Directory {
|
func (m *mockAcmeAuthority) GetDirectory(p provisioner.Interface, baseURLFromRequest string) *acme.Directory {
|
||||||
if m.getDirectory != nil {
|
if m.getDirectory != nil {
|
||||||
return m.getDirectory(p)
|
return m.getDirectory(p, baseURLFromRequest)
|
||||||
}
|
}
|
||||||
return m.ret1.(*acme.Directory)
|
return m.ret1.(*acme.Directory)
|
||||||
}
|
}
|
||||||
|
@ -276,6 +276,7 @@ func TestHandlerGetDirectory(t *testing.T) {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
h := New(auth).(*Handler)
|
h := New(auth).(*Handler)
|
||||||
req := httptest.NewRequest("GET", url, nil)
|
req := httptest.NewRequest("GET", url, nil)
|
||||||
|
req.Header.Add("X-Forwarded-Proto", "https")
|
||||||
req = req.WithContext(tc.ctx)
|
req = req.WithContext(tc.ctx)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
h.GetDirectory(w, req)
|
h.GetDirectory(w, req)
|
||||||
|
|
28
acme/api/hostutil.go
Normal file
28
acme/api/hostutil.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// baseURLFromRequest determines the base URL which should be used for constructing link URLs in e.g. the ACME directory
|
||||||
|
// result by taking the request Host, TLS and Header[X-Forwarded-Proto] values into consideration.
|
||||||
|
// If the Request.Host is an empty string, we return an empty string, to indicate that the configured
|
||||||
|
// URL values should be used instead.
|
||||||
|
// If this function returns a non-empty result, then this should be used in constructing ACME link URLs.
|
||||||
|
func baseURLFromRequest(r *http.Request) string {
|
||||||
|
// TODO: I semantically copied the functionality of determining the protol from boulder web/relative.go
|
||||||
|
// which allows HTTP. Previously this was always forced to be HTTPS for absolute URLs. Should this be
|
||||||
|
// changed to also always force HTTPS protocol?
|
||||||
|
proto := "http"
|
||||||
|
if specifiedProto := r.Header.Get("X-Forwarded-Proto"); specifiedProto != "" {
|
||||||
|
proto = specifiedProto
|
||||||
|
} else if r.TLS != nil {
|
||||||
|
proto += "s"
|
||||||
|
}
|
||||||
|
|
||||||
|
host := r.Host
|
||||||
|
if host == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return proto + "://" + host
|
||||||
|
}
|
70
acme/api/hostutil_test.go
Normal file
70
acme/api/hostutil_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetBaseUrl(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
testFailedDescription string
|
||||||
|
targetURL string
|
||||||
|
expectedResult string
|
||||||
|
requestPreparer func(*http.Request)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"HTTP host pass-through failed.",
|
||||||
|
"http://my.dummy.host",
|
||||||
|
"http://my.dummy.host",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"HTTPS host pass-through failed.",
|
||||||
|
"https://my.dummy.host",
|
||||||
|
"https://my.dummy.host",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Port pass-through failed",
|
||||||
|
"http://host.with.port:8080",
|
||||||
|
"http://host.with.port:8080",
|
||||||
|
nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Explicit host from Request.Host was not used.",
|
||||||
|
"http://some.target.host:8080",
|
||||||
|
"http://proxied.host",
|
||||||
|
func(r *http.Request) {
|
||||||
|
r.Host = "proxied.host"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Explicit forwarded protocol from request header X-Forwarded-Proto was not used.",
|
||||||
|
"http://some.host",
|
||||||
|
"ssl://some.host",
|
||||||
|
func(r *http.Request) {
|
||||||
|
r.Header.Add("X-Forwarded-Proto", "ssl")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Missing Request.Host value did not result in empty string result.",
|
||||||
|
"http://some.host",
|
||||||
|
"",
|
||||||
|
func(r *http.Request) {
|
||||||
|
r.Host = ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
request := httptest.NewRequest("GET", test.targetURL, nil)
|
||||||
|
if test.requestPreparer != nil {
|
||||||
|
test.requestPreparer(request)
|
||||||
|
}
|
||||||
|
result := baseURLFromRequest(request)
|
||||||
|
if result != test.expectedResult {
|
||||||
|
t.Errorf("Expected %q, but got %q", test.expectedResult, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ type Interface interface {
|
||||||
GetAccountByKey(provisioner.Interface, *jose.JSONWebKey) (*Account, error)
|
GetAccountByKey(provisioner.Interface, *jose.JSONWebKey) (*Account, error)
|
||||||
GetAuthz(provisioner.Interface, string, string) (*Authz, error)
|
GetAuthz(provisioner.Interface, string, string) (*Authz, error)
|
||||||
GetCertificate(string, string) ([]byte, error)
|
GetCertificate(string, string) ([]byte, error)
|
||||||
GetDirectory(provisioner.Interface) *Directory
|
GetDirectory(provisioner.Interface, string) *Directory
|
||||||
GetLink(Link, string, bool, ...string) string
|
GetLink(Link, string, bool, ...string) string
|
||||||
GetOrder(provisioner.Interface, string, string) (*Order, error)
|
GetOrder(provisioner.Interface, string, string) (*Order, error)
|
||||||
GetOrdersByAccount(provisioner.Interface, string) ([]string, error)
|
GetOrdersByAccount(provisioner.Interface, string) ([]string, error)
|
||||||
|
@ -82,14 +82,14 @@ func (a *Authority) GetLink(typ Link, provID string, abs bool, inputs ...string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDirectory returns the ACME directory object.
|
// GetDirectory returns the ACME directory object.
|
||||||
func (a *Authority) GetDirectory(p provisioner.Interface) *Directory {
|
func (a *Authority) GetDirectory(p provisioner.Interface, baseURLFromRequest string) *Directory {
|
||||||
name := url.PathEscape(p.GetName())
|
name := url.PathEscape(p.GetName())
|
||||||
return &Directory{
|
return &Directory{
|
||||||
NewNonce: a.dir.getLink(NewNonceLink, name, true),
|
NewNonce: a.dir.getLinkFromBaseURL(NewNonceLink, name, true, baseURLFromRequest),
|
||||||
NewAccount: a.dir.getLink(NewAccountLink, name, true),
|
NewAccount: a.dir.getLinkFromBaseURL(NewAccountLink, name, true, baseURLFromRequest),
|
||||||
NewOrder: a.dir.getLink(NewOrderLink, name, true),
|
NewOrder: a.dir.getLinkFromBaseURL(NewOrderLink, name, true, baseURLFromRequest),
|
||||||
RevokeCert: a.dir.getLink(RevokeCertLink, name, true),
|
RevokeCert: a.dir.getLinkFromBaseURL(RevokeCertLink, name, true, baseURLFromRequest),
|
||||||
KeyChange: a.dir.getLink(KeyChangeLink, name, true),
|
KeyChange: a.dir.getLinkFromBaseURL(KeyChangeLink, name, true, baseURLFromRequest),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -73,7 +73,7 @@ func TestAuthorityGetDirectory(t *testing.T) {
|
||||||
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||||
assert.FatalError(t, err)
|
assert.FatalError(t, err)
|
||||||
prov := newProv()
|
prov := newProv()
|
||||||
acmeDir := auth.GetDirectory(prov)
|
acmeDir := auth.GetDirectory(prov, "")
|
||||||
assert.Equals(t, acmeDir.NewNonce, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-nonce", URLSafeProvisionerName(prov)))
|
assert.Equals(t, acmeDir.NewNonce, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-nonce", URLSafeProvisionerName(prov)))
|
||||||
assert.Equals(t, acmeDir.NewAccount, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-account", URLSafeProvisionerName(prov)))
|
assert.Equals(t, acmeDir.NewAccount, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-account", URLSafeProvisionerName(prov)))
|
||||||
assert.Equals(t, acmeDir.NewOrder, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-order", URLSafeProvisionerName(prov)))
|
assert.Equals(t, acmeDir.NewOrder, fmt.Sprintf("https://ca.smallstep.com/acme/%s/new-order", URLSafeProvisionerName(prov)))
|
||||||
|
@ -82,6 +82,20 @@ func TestAuthorityGetDirectory(t *testing.T) {
|
||||||
assert.Equals(t, acmeDir.KeyChange, fmt.Sprintf("https://ca.smallstep.com/acme/%s/key-change", URLSafeProvisionerName(prov)))
|
assert.Equals(t, acmeDir.KeyChange, fmt.Sprintf("https://ca.smallstep.com/acme/%s/key-change", URLSafeProvisionerName(prov)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthorityGetDirectoryWithBaseURL(t *testing.T) {
|
||||||
|
baseURL := "http://my.proxied.host"
|
||||||
|
auth, err := NewAuthority(new(db.MockNoSQLDB), "ca.smallstep.com", "acme", nil)
|
||||||
|
assert.FatalError(t, err)
|
||||||
|
prov := newProv()
|
||||||
|
acmeDir := auth.GetDirectory(prov, baseURL)
|
||||||
|
assert.Equals(t, acmeDir.NewNonce, fmt.Sprintf("%s/acme/%s/new-nonce", baseURL, URLSafeProvisionerName(prov)))
|
||||||
|
assert.Equals(t, acmeDir.NewAccount, fmt.Sprintf("%s/acme/%s/new-account", baseURL, URLSafeProvisionerName(prov)))
|
||||||
|
assert.Equals(t, acmeDir.NewOrder, fmt.Sprintf("%s/acme/%s/new-order", baseURL, URLSafeProvisionerName(prov)))
|
||||||
|
//assert.Equals(t, acmeDir.NewOrder, "%s/acme/new-authz")
|
||||||
|
assert.Equals(t, acmeDir.RevokeCert, fmt.Sprintf("%s/acme/%s/revoke-cert", baseURL, URLSafeProvisionerName(prov)))
|
||||||
|
assert.Equals(t, acmeDir.KeyChange, fmt.Sprintf("%s/acme/%s/key-change", baseURL, URLSafeProvisionerName(prov)))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthorityNewNonce(t *testing.T) {
|
func TestAuthorityNewNonce(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
auth *Authority
|
auth *Authority
|
||||||
|
|
|
@ -102,6 +102,12 @@ func (l Link) String() string {
|
||||||
|
|
||||||
// getLink returns an absolute or partial path to the given resource.
|
// getLink returns an absolute or partial path to the given resource.
|
||||||
func (d *directory) getLink(typ Link, provisionerName string, abs bool, inputs ...string) string {
|
func (d *directory) getLink(typ Link, provisionerName string, abs bool, inputs ...string) string {
|
||||||
|
return d.getLinkFromBaseURL(typ, provisionerName, abs, "", inputs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLinkFromBaseURL returns an absolute or partial path to the given resource and a base URL dynamically obtained from the request for which
|
||||||
|
// the link is being calculated.
|
||||||
|
func (d *directory) getLinkFromBaseURL(typ Link, provisionerName string, abs bool, baseURLFromRequest string, inputs ...string) string {
|
||||||
var link string
|
var link string
|
||||||
switch typ {
|
switch typ {
|
||||||
case NewNonceLink, NewAccountLink, NewOrderLink, NewAuthzLink, DirectoryLink, KeyChangeLink, RevokeCertLink:
|
case NewNonceLink, NewAccountLink, NewOrderLink, NewAuthzLink, DirectoryLink, KeyChangeLink, RevokeCertLink:
|
||||||
|
@ -114,7 +120,11 @@ func (d *directory) getLink(typ Link, provisionerName string, abs bool, inputs .
|
||||||
link = fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLink.String(), inputs[0])
|
link = fmt.Sprintf("/%s/%s/%s/finalize", provisionerName, OrderLink.String(), inputs[0])
|
||||||
}
|
}
|
||||||
if abs {
|
if abs {
|
||||||
return fmt.Sprintf("https://%s/%s%s", d.dns, d.prefix, link)
|
baseURL := baseURLFromRequest
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "https://" + d.dns
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s%s", baseURL, d.prefix, link)
|
||||||
}
|
}
|
||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue