2015-06-10 22:12:46 +00:00
package acme
2015-06-13 01:55:53 +00:00
import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
2015-06-13 16:37:30 +00:00
"io/ioutil"
2015-06-13 01:55:53 +00:00
"net"
"net/http"
2015-06-13 17:13:04 +00:00
"strings"
2015-06-13 01:55:53 +00:00
"time"
)
type simpleHTTPChallenge struct {
jws * jws
optPort string
}
2015-06-10 22:12:46 +00:00
2015-06-13 16:37:30 +00:00
// SimpleHTTPS checks for DNS, public IP and port bindings
func ( s * simpleHTTPChallenge ) CanSolve ( domain string ) bool {
2015-09-26 17:45:52 +00:00
2015-06-13 16:37:30 +00:00
// determine public ip
resp , err := http . Get ( "https://icanhazip.com/" )
if err != nil {
logger ( ) . Printf ( "Could not get public IP -> %v" , err )
return false
}
ip , err := ioutil . ReadAll ( resp . Body )
if err != nil {
logger ( ) . Printf ( "Could not get public IP -> %v" , err )
return false
}
ipStr := string ( ip )
2015-06-13 17:13:04 +00:00
ipStr = strings . Replace ( ipStr , "\n" , "" , - 1 )
2015-06-13 16:37:30 +00:00
// resolve domain we should solve for
resolvedIPs , err := net . LookupHost ( domain )
if err != nil {
logger ( ) . Printf ( "Could not lookup DNS A record for %s" , domain )
return false
}
// if the resolve does not resolve to our public ip, we can't solve.
for _ , resolvedIP := range resolvedIPs {
if resolvedIP == ipStr {
return true
}
}
2015-09-26 17:45:52 +00:00
logger ( ) . Printf ( "SimpleHTTP: Domain %s does not resolve to the public ip of this server. Determined IP: %s Resolved IP: %s" , domain , ipStr , resolvedIPs [ 0 ] )
2015-06-13 16:37:30 +00:00
return false
2015-06-10 22:12:46 +00:00
}
2015-06-13 01:55:53 +00:00
func ( s * simpleHTTPChallenge ) Solve ( chlng challenge , domain string ) error {
2015-09-26 17:45:52 +00:00
logger ( ) . Print ( "Trying to solve SimpleHTTP" )
2015-06-13 01:55:53 +00:00
2015-06-13 02:26:33 +00:00
// Generate random string for the path. The acme server will
// access this path on the server in order to validate the request
2015-09-26 17:45:52 +00:00
listener , err := s . startHTTPSServer ( domain , chlng . Token )
2015-06-13 01:55:53 +00:00
if err != nil {
return fmt . Errorf ( "Could not start HTTPS server for challenge -> %v" , err )
}
2015-06-13 02:26:33 +00:00
// Tell the server about the generated random path
2015-09-26 17:45:52 +00:00
jsonBytes , err := json . Marshal ( challenge { Resource : "challenge" , Type : chlng . Type , Token : chlng . Token } )
2015-06-13 01:55:53 +00:00
if err != nil {
return errors . New ( "Failed to marshal network message..." )
}
resp , err := s . jws . post ( chlng . URI , jsonBytes )
if err != nil {
return fmt . Errorf ( "Failed to post JWS message. -> %v" , err )
}
2015-06-13 02:26:33 +00:00
// After the path is sent, the ACME server will access our server.
// Repeatedly check the server for an updated status on our request.
2015-06-13 01:55:53 +00:00
var challengeResponse challenge
loop :
for {
decoder := json . NewDecoder ( resp . Body )
decoder . Decode ( & challengeResponse )
switch challengeResponse . Status {
case "valid" :
logger ( ) . Print ( "The server validated our request" )
listener . Close ( )
break loop
case "pending" :
break
case "invalid" :
2015-06-13 19:06:47 +00:00
listener . Close ( )
2015-09-26 17:45:52 +00:00
logger ( ) . Print ( "The server could not validate our request." )
2015-06-13 01:55:53 +00:00
return errors . New ( "The server could not validate our request." )
default :
2015-06-13 19:06:47 +00:00
listener . Close ( )
2015-09-26 17:45:52 +00:00
logger ( ) . Print ( "The server returned an unexpected state." )
2015-06-13 01:55:53 +00:00
return errors . New ( "The server returned an unexpected state." )
}
time . Sleep ( 1 * time . Second )
resp , err = http . Get ( chlng . URI )
}
return nil
}
// Starts a temporary HTTPS server on port 443. As soon as the challenge passed validation,
// this server will get shut down. The certificate generated here is only held in memory.
2015-09-26 17:45:52 +00:00
func ( s * simpleHTTPChallenge ) startHTTPSServer ( domain string , token string ) ( net . Listener , error ) {
2015-06-13 02:26:33 +00:00
// Generate a new RSA key and a self-signed certificate.
2015-06-13 01:55:53 +00:00
tempPrivKey , err := generatePrivateKey ( 2048 )
if err != nil {
return nil , err
}
2015-10-16 19:05:16 +00:00
tempCertPEM , err := generatePemCert ( tempPrivKey , domain )
2015-06-13 01:55:53 +00:00
if err != nil {
return nil , err
}
pemBytes := pem . EncodeToMemory ( & pem . Block { Type : "RSA PRIVATE KEY" , Bytes : x509 . MarshalPKCS1PrivateKey ( tempPrivKey ) } )
tempKeyPair , err := tls . X509KeyPair (
tempCertPEM ,
pemBytes )
if err != nil {
return nil , err
}
tlsConf := new ( tls . Config )
tlsConf . Certificates = [ ] tls . Certificate { tempKeyPair }
2015-09-26 17:45:52 +00:00
path := "/.well-known/acme-challenge/" + token
2015-06-13 01:55:53 +00:00
2015-06-13 02:26:33 +00:00
// Allow for CLI override
2015-06-13 01:55:53 +00:00
port := ":443"
if s . optPort != "" {
port = ":" + s . optPort
}
2015-06-13 02:26:33 +00:00
2015-06-13 01:55:53 +00:00
tlsListener , err := tls . Listen ( "tcp" , port , tlsConf )
if err != nil {
2015-06-13 19:15:34 +00:00
return nil , fmt . Errorf ( "Could not start HTTP listener! -> %v" , err )
2015-06-13 01:55:53 +00:00
}
2015-09-26 17:45:52 +00:00
jsonBytes , err := json . Marshal ( challenge { Type : "simpleHttp" , Token : token , Tls : true } )
if err != nil {
return nil , errors . New ( "startHTTPSServer: Failed to marshal network message..." )
}
signed , err := s . jws . signContent ( jsonBytes )
if err != nil {
return nil , errors . New ( "startHTTPSServer: Failed to sign message..." )
}
signedCompact := signed . FullSerialize ( )
if err != nil {
return nil , errors . New ( "startHTTPSServer: Failed to serialize message..." )
}
2015-06-13 02:26:33 +00:00
// The handler validates the HOST header and request type.
// For validation it then writes the token the server returned with the challenge
2015-06-13 01:55:53 +00:00
http . HandleFunc ( path , func ( w http . ResponseWriter , r * http . Request ) {
2015-09-26 17:45:52 +00:00
if strings . HasPrefix ( r . Host , domain ) && r . Method == "GET" {
w . Header ( ) . Add ( "Content-Type" , "application/jose+json" )
w . Write ( [ ] byte ( signedCompact ) )
logger ( ) . Print ( "Served JWS payload..." )
} else {
logger ( ) . Printf ( "Received request for domain %s with method %s" , r . Host , r . Method )
w . Write ( [ ] byte ( "TEST" ) )
2015-06-13 01:55:53 +00:00
}
} )
go http . Serve ( tlsListener , nil )
return tlsListener , nil
}
func getRandomString ( length int ) string {
const alphanum = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
var bytes = make ( [ ] byte , length )
rand . Read ( bytes )
for i , b := range bytes {
bytes [ i ] = alphanum [ b % byte ( len ( alphanum ) ) ]
}
return string ( bytes )
}