add DNS-01 API relay
This commit is contained in:
parent
1f22f5f34f
commit
710771c228
|
@ -4,6 +4,7 @@ certs
|
||||||
acme.d
|
acme.d
|
||||||
xversion.go
|
xversion.go
|
||||||
|
|
||||||
|
/mplexer/cmd/dnsclient/dnsclient
|
||||||
/mplexer/cmd/mgmt/mgmt
|
/mplexer/cmd/mgmt/mgmt
|
||||||
/mplexer/cmd/signjwt/signjwt
|
/mplexer/cmd/signjwt/signjwt
|
||||||
/mplexer/cmd/telebit/telebit
|
/mplexer/cmd/telebit/telebit
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -11,5 +11,6 @@ require (
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
github.com/joho/godotenv v1.3.0
|
github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/stretchr/testify v1.5.1
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
dns01 "git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
"github.com/go-acme/lego/v3/challenge"
|
||||||
|
_ "github.com/joho/godotenv/autoload"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// GitRev refers to the abbreviated commit hash
|
||||||
|
GitRev = "0000000"
|
||||||
|
// GitVersion refers to the most recent tag, plus any commits made since then
|
||||||
|
GitVersion = "v0.0.0-pre0+0000000"
|
||||||
|
// GitTimestamp refers to the timestamp of the most recent commit
|
||||||
|
GitTimestamp = "0000-00-00T00:00:00+0000"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var err error
|
||||||
|
var provider challenge.Provider = nil
|
||||||
|
var domains []string
|
||||||
|
|
||||||
|
// TODO replace the websocket connection with a mock server
|
||||||
|
acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay")
|
||||||
|
secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
|
||||||
|
token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if len(os.Args) >= 2 {
|
||||||
|
if "version" == os.Args[1] {
|
||||||
|
fmt.Printf("telebit %s %s %s", GitVersion, GitRev, GitTimestamp)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == *token {
|
||||||
|
if "" == *secret {
|
||||||
|
*secret = os.Getenv("SECRET")
|
||||||
|
}
|
||||||
|
*token, err = getToken(*secret, domains)
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "neither secret nor token provided")
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == *acmeRelay {
|
||||||
|
panic(errors.New("ACME relay should be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint := *acmeRelay
|
||||||
|
if strings.HasSuffix(endpoint, "/") {
|
||||||
|
endpoint = endpoint[:len(endpoint)-1]
|
||||||
|
}
|
||||||
|
endpoint += "/api/dns/"
|
||||||
|
if provider, err = newAPIDNSProvider(endpoint, *token); nil != err {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = provider.Present(os.Getenv("HOSTNAME"), "xxx", "yyy")
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
}
|
||||||
|
err = provider.Present(os.Getenv("HOSTNAME"), "xxx", "yyy")
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("quite possibly successful")
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAPIDNSProvider is for the sake of demoing the tunnel
|
||||||
|
func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) {
|
||||||
|
config := dns01.NewDefaultConfig()
|
||||||
|
config.Token = token
|
||||||
|
endpoint, err := url.Parse(baseURL)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Endpoint = endpoint
|
||||||
|
return dns01.NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getToken(secret string, domains []string) (token string, err error) {
|
||||||
|
tokenData := jwt.MapClaims{"domains": domains}
|
||||||
|
|
||||||
|
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData)
|
||||||
|
if token, err = jwtToken.SignedString([]byte(secret)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token, nil
|
||||||
|
}
|
|
@ -7,11 +7,14 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
|
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
|
||||||
|
dns01 "git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
|
||||||
|
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
jwt "github.com/dgrijalva/jwt-go"
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
@ -39,7 +42,6 @@ type Forward struct {
|
||||||
func main() {
|
func main() {
|
||||||
var err error
|
var err error
|
||||||
var provider challenge.Provider = nil
|
var provider challenge.Provider = nil
|
||||||
var enableTLSALPN01 bool
|
|
||||||
var domains []string
|
var domains []string
|
||||||
var forwards []Forward
|
var forwards []Forward
|
||||||
|
|
||||||
|
@ -50,6 +52,8 @@ func main() {
|
||||||
acmeStaging := flag.Bool("acme-staging", false, "get fake certificates for testing")
|
acmeStaging := flag.Bool("acme-staging", false, "get fake certificates for testing")
|
||||||
acmeDirectory := flag.String("acme-directory", "", "ACME Directory URL")
|
acmeDirectory := flag.String("acme-directory", "", "ACME Directory URL")
|
||||||
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
|
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
|
||||||
|
enableTLSALPN01 := flag.Bool("acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
|
||||||
|
acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay")
|
||||||
relay := flag.String("relay", "", "the domain (or ip address) at which the relay server is running")
|
relay := flag.String("relay", "", "the domain (or ip address) at which the relay server is running")
|
||||||
secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
|
secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
|
||||||
token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
|
token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
|
||||||
|
@ -98,6 +102,30 @@ func main() {
|
||||||
domains = append(domains, domain)
|
domains = append(domains, domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if "" == *token {
|
||||||
|
if "" == *secret {
|
||||||
|
*secret = os.Getenv("SECRET")
|
||||||
|
}
|
||||||
|
*token, err = getToken(*secret, domains)
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "neither secret nor token provided")
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if "" == *relay {
|
||||||
|
*relay = os.Getenv("RELAY") // "wss://example.com:443"
|
||||||
|
}
|
||||||
|
if "" == *relay {
|
||||||
|
fmt.Fprintf(os.Stderr, "Missing relay url")
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if "" == *acmeRelay {
|
||||||
|
*acmeRelay = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443"
|
||||||
|
}
|
||||||
|
|
||||||
if "" != os.Getenv("GODADDY_API_KEY") {
|
if "" != os.Getenv("GODADDY_API_KEY") {
|
||||||
id := os.Getenv("GODADDY_API_KEY")
|
id := os.Getenv("GODADDY_API_KEY")
|
||||||
secret := os.Getenv("GODADDY_API_SECRET")
|
secret := os.Getenv("GODADDY_API_SECRET")
|
||||||
|
@ -109,23 +137,15 @@ func main() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enableTLSALPN01 = true
|
endpoint := *acmeRelay
|
||||||
}
|
if strings.HasSuffix(endpoint, "/") {
|
||||||
|
endpoint = endpoint[:len(endpoint)-1]
|
||||||
if "" == *relay {
|
}
|
||||||
*relay = os.Getenv("RELAY") // "wss://example.com:443"
|
endpoint += "/api/dns/"
|
||||||
}
|
if provider, err = newAPIDNSProvider(endpoint, *token); nil != err {
|
||||||
if "" == *token {
|
panic(err)
|
||||||
if "" == *secret {
|
|
||||||
*secret = os.Getenv("SECRET")
|
|
||||||
}
|
}
|
||||||
*token, err = getToken(*secret, domains)
|
|
||||||
}
|
}
|
||||||
if nil != err {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
acme := &telebit.ACME{
|
acme := &telebit.ACME{
|
||||||
Email: *email,
|
Email: *email,
|
||||||
|
@ -134,7 +154,7 @@ func main() {
|
||||||
Directory: *acmeDirectory,
|
Directory: *acmeDirectory,
|
||||||
DNSProvider: provider,
|
DNSProvider: provider,
|
||||||
EnableHTTPChallenge: *enableHTTP01,
|
EnableHTTPChallenge: *enableHTTP01,
|
||||||
EnableTLSALPNChallenge: enableTLSALPN01,
|
EnableTLSALPNChallenge: *enableTLSALPN01,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := telebit.NewRouteMux()
|
mux := telebit.NewRouteMux()
|
||||||
|
@ -144,13 +164,24 @@ func main() {
|
||||||
//mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second)
|
//mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
tun, err := telebit.DialWebsocketTunnel(ctx, *relay, *token)
|
connected := make(chan net.Conn)
|
||||||
if nil != err {
|
go func() {
|
||||||
fmt.Println("relay:", relay)
|
timeoutCtx, cancelTimeout := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
|
||||||
log.Fatal(err)
|
tun, err := telebit.DialWebsocketTunnel(timeoutCtx, *relay, *token)
|
||||||
return
|
if nil != err {
|
||||||
}
|
msg := ""
|
||||||
|
if strings.Contains(err.Error(), "bad handshake") {
|
||||||
|
msg = " (may be auth related)"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Error connecting to %s: %s%s\n", *relay, err, msg)
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancelTimeout()
|
||||||
|
connected <- tun
|
||||||
|
}()
|
||||||
|
|
||||||
|
tun := <-connected
|
||||||
fmt.Printf("Listening at %s\n", *relay)
|
fmt.Printf("Listening at %s\n", *relay)
|
||||||
log.Fatal("Closed server: ", telebit.ListenAndServe(tun, mux))
|
log.Fatal("Closed server: ", telebit.ListenAndServe(tun, mux))
|
||||||
}
|
}
|
||||||
|
@ -183,6 +214,18 @@ func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) {
|
||||||
return godaddy.NewDNSProviderConfig(config)
|
return godaddy.NewDNSProviderConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newAPIDNSProvider is for the sake of demoing the tunnel
|
||||||
|
func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) {
|
||||||
|
config := dns01.NewDefaultConfig()
|
||||||
|
config.Token = token
|
||||||
|
endpoint, err := url.Parse(baseURL)
|
||||||
|
if nil != err {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.Endpoint = endpoint
|
||||||
|
return dns01.NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// TODO for http proxy
|
// TODO for http proxy
|
||||||
return mplexer.TargetOptions {
|
return mplexer.TargetOptions {
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2020 AJ ONeal
|
||||||
|
Copyright (c) 2015-2017 Sebastian Erhart
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,182 @@
|
||||||
|
// Package dns01 implements a DNS provider for solving the DNS-01 challenge through a HTTP server.
|
||||||
|
package dns01
|
||||||
|
|
||||||
|
// Adapted from https://github.com/go-acme/lego/blob/master/providers/dns/httpreq/httpreq.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v3/challenge/dns01"
|
||||||
|
"github.com/go-acme/lego/v3/platform/config/env"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Environment variables names.
|
||||||
|
const (
|
||||||
|
envNamespace = "API_"
|
||||||
|
|
||||||
|
EnvEndpoint = envNamespace + "ENDPOINT"
|
||||||
|
EnvToken = envNamespace + "TOKEN"
|
||||||
|
|
||||||
|
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
|
||||||
|
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
|
||||||
|
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dnsChallenge struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
KeyAuth string `json:"key_authorization"`
|
||||||
|
KeyAuthDigest string `json:"key_authorization_digest"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is used to configure the creation of the DNSProvider.
|
||||||
|
type Config struct {
|
||||||
|
Endpoint *url.URL
|
||||||
|
Token string
|
||||||
|
PropagationTimeout time.Duration
|
||||||
|
PollingInterval time.Duration
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultConfig returns a default configuration for the DNSProvider.
|
||||||
|
func NewDefaultConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
|
||||||
|
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 15*time.Second),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSProvider implements the challenge.Provider interface.
|
||||||
|
type DNSProvider struct {
|
||||||
|
config *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProvider returns a DNSProvider instance.
|
||||||
|
func NewDNSProvider() (*DNSProvider, error) {
|
||||||
|
values, err := env.Get(EnvEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("api: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := url.Parse(values[EnvEndpoint])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("api: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Token = env.GetOrFile(EnvToken)
|
||||||
|
config.Endpoint = endpoint
|
||||||
|
return NewDNSProviderConfig(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSProviderConfig return a DNSProvider.
|
||||||
|
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("api: the configuration of the DNS provider is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Endpoint == nil {
|
||||||
|
return nil, errors.New("api: the endpoint is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSProvider{config: config}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout returns the timeout and interval to use when checking for DNS propagation.
|
||||||
|
// Adjusting here to cope with spikes in propagation times.
|
||||||
|
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
|
||||||
|
return d.config.PropagationTimeout, d.config.PollingInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present creates a TXT record to fulfill the dns-01 challenge.
|
||||||
|
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
|
||||||
|
msg := getDNSChallenge(domain, token, keyAuth)
|
||||||
|
|
||||||
|
err := d.doRequest(http.MethodPost, fmt.Sprintf("/%s", msg.Domain), msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("api: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanUp removes the TXT record matching the specified parameters.
|
||||||
|
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
|
||||||
|
msg := getDNSChallenge(domain, token, keyAuth)
|
||||||
|
|
||||||
|
err := d.doRequest(
|
||||||
|
http.MethodDelete,
|
||||||
|
fmt.Sprintf("/%s/%s/%s", msg.Domain, msg.Token, msg.KeyAuth),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("api: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDNSChallenge(domain, token, keyAuth string) *dnsChallenge {
|
||||||
|
hostname, digest := dns01.GetRecord(domain, keyAuth)
|
||||||
|
return &dnsChallenge{
|
||||||
|
Domain: domain,
|
||||||
|
Hostname: hostname,
|
||||||
|
Token: token,
|
||||||
|
KeyAuth: keyAuth,
|
||||||
|
KeyAuthDigest: digest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSProvider) doRequest(method, uri string, msg interface{}) error {
|
||||||
|
reqBody := &bytes.Buffer{}
|
||||||
|
if nil != msg {
|
||||||
|
err := json.NewEncoder(reqBody).Encode(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newURI := path.Join(d.config.Endpoint.EscapedPath(), uri)
|
||||||
|
endpoint, err := d.config.Endpoint.Parse(newURI)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, endpoint.String(), reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if len(d.config.Token) > 0 {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+d.config.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := d.config.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%d: failed to read response body: %w", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%d: request failed: %v", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,296 @@
|
||||||
|
package dns01
|
||||||
|
|
||||||
|
// Adapted from https://github.com/go-acme/lego/blob/master/providers/dns/httpreq/httpreq.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-acme/lego/v3/platform/tester"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
var envTest = tester.NewEnvTest(EnvEndpoint, EnvToken)
|
||||||
|
|
||||||
|
func TestNewDNSProvider(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
envVars map[string]string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvEndpoint: "http://localhost:8090",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "invalid URL",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvEndpoint: ":",
|
||||||
|
},
|
||||||
|
expected: `api: parse ":": missing protocol scheme`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing endpoint",
|
||||||
|
envVars: map[string]string{
|
||||||
|
EnvEndpoint: "",
|
||||||
|
},
|
||||||
|
expected: "api: some credentials information are missing: API_ENDPOINT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
defer envTest.RestoreEnv()
|
||||||
|
envTest.ClearEnv()
|
||||||
|
|
||||||
|
envTest.Apply(test.envVars)
|
||||||
|
|
||||||
|
p, err := NewDNSProvider()
|
||||||
|
|
||||||
|
if len(test.expected) == 0 {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
require.NotNil(t, p.config)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProviderConfig(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
endpoint *url.URL
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
endpoint: mustParse("http://localhost:8090"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "missing endpoint",
|
||||||
|
expected: "api: the endpoint is missing",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Endpoint = test.endpoint
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
|
||||||
|
if len(test.expected) == 0 {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, p)
|
||||||
|
require.NotNil(t, p.config)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProvider_Present(t *testing.T) {
|
||||||
|
envTest.RestoreEnv()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
token string
|
||||||
|
pathPrefix string
|
||||||
|
handler http.HandlerFunc
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
handler: successHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success with path prefix",
|
||||||
|
handler: successHandler,
|
||||||
|
pathPrefix: "/api/acme/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "error",
|
||||||
|
handler: http.NotFound,
|
||||||
|
expectedError: "api: 404: request failed: 404 page not found\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success raw mode",
|
||||||
|
handler: successRawModeHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "error raw mode",
|
||||||
|
handler: http.NotFound,
|
||||||
|
expectedError: "api: 404: request failed: 404 page not found\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bearer auth",
|
||||||
|
token: "foobar",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
token := strings.Replace(req.Header.Get("Authorization"), "Bearer ", "", 1)
|
||||||
|
if token != "foobar" {
|
||||||
|
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password."))
|
||||||
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(rw, "lego")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
hostname := "domain"
|
||||||
|
mux.HandleFunc(path.Join("/", test.pathPrefix, "/"+hostname), test.handler)
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Endpoint = mustParse(server.URL + test.pathPrefix)
|
||||||
|
config.Token = test.token
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = p.Present("domain", "token", "key")
|
||||||
|
if test.expectedError == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expectedError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDNSProvider_Cleanup(t *testing.T) {
|
||||||
|
envTest.RestoreEnv()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
desc string
|
||||||
|
token string
|
||||||
|
handler http.HandlerFunc
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "success",
|
||||||
|
handler: successHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "error",
|
||||||
|
handler: http.NotFound,
|
||||||
|
expectedError: "api: 404: request failed: 404 page not found\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "success raw mode",
|
||||||
|
handler: successRawModeHandler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "error raw mode",
|
||||||
|
handler: http.NotFound,
|
||||||
|
expectedError: "api: 404: request failed: 404 page not found\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "basic auth",
|
||||||
|
token: "foobar",
|
||||||
|
handler: func(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
token := strings.Replace(req.Header.Get("Authorization"), "Bearer ", "", 1)
|
||||||
|
if token != "foobar" {
|
||||||
|
rw.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, "Please enter your username and password."))
|
||||||
|
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprint(rw, "lego")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testCases {
|
||||||
|
test := test
|
||||||
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hostname := "domain"
|
||||||
|
dnsToken := "token"
|
||||||
|
dnsKeyAuth := "key"
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc(
|
||||||
|
fmt.Sprintf("/%s/%s/%s", hostname, dnsToken, dnsKeyAuth),
|
||||||
|
test.handler,
|
||||||
|
)
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
|
||||||
|
config := NewDefaultConfig()
|
||||||
|
config.Endpoint = mustParse(server.URL)
|
||||||
|
config.Token = test.token
|
||||||
|
|
||||||
|
p, err := NewDNSProviderConfig(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = p.CleanUp("domain", "token", "key")
|
||||||
|
if test.expectedError == "" {
|
||||||
|
require.NoError(t, err)
|
||||||
|
} else {
|
||||||
|
require.EqualError(t, err, test.expectedError)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func successHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost && req.Method != http.MethodDelete {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &dnsChallenge{}
|
||||||
|
err := json.NewDecoder(req.Body).Decode(msg)
|
||||||
|
if err != nil {
|
||||||
|
if !(req.Method == http.MethodDelete && io.EOF == err) {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(rw, "lego")
|
||||||
|
}
|
||||||
|
|
||||||
|
func successRawModeHandler(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
if req.Method != http.MethodPost && req.Method != http.MethodDelete {
|
||||||
|
http.Error(rw, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &dnsChallenge{}
|
||||||
|
err := json.NewDecoder(req.Body).Decode(msg)
|
||||||
|
if err != nil {
|
||||||
|
if !(req.Method == http.MethodDelete && io.EOF == err) {
|
||||||
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(rw, "lego")
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustParse(rawURL string) *url.URL {
|
||||||
|
uri, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return uri
|
||||||
|
}
|
|
@ -40,7 +40,7 @@ func Listen(tun net.Conn) *Listener {
|
||||||
go func() {
|
go func() {
|
||||||
err := listener.encoder.Run()
|
err := listener.encoder.Run()
|
||||||
fmt.Printf("encoder stopped entirely: %q", err)
|
fmt.Printf("encoder stopped entirely: %q", err)
|
||||||
tun.Close()
|
listener.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Decode the stream as it comes in
|
// Decode the stream as it comes in
|
||||||
|
|
|
@ -12,7 +12,7 @@ import (
|
||||||
// and selects the matching handler.
|
// and selects the matching handler.
|
||||||
type RouteMux struct {
|
type RouteMux struct {
|
||||||
defaultTimeout time.Duration
|
defaultTimeout time.Duration
|
||||||
list []meta
|
routes []meta
|
||||||
}
|
}
|
||||||
|
|
||||||
type meta struct {
|
type meta struct {
|
||||||
|
@ -34,7 +34,7 @@ func (m *RouteMux) Serve(client net.Conn) error {
|
||||||
wconn := &ConnWrap{Conn: client}
|
wconn := &ConnWrap{Conn: client}
|
||||||
servername := wconn.Servername()
|
servername := wconn.Servername()
|
||||||
|
|
||||||
for _, meta := range m.list {
|
for _, meta := range m.routes {
|
||||||
if servername == meta.addr || "*" == meta.addr {
|
if servername == meta.addr || "*" == meta.addr {
|
||||||
//fmt.Println("[debug] test of route:", meta)
|
//fmt.Println("[debug] test of route:", meta)
|
||||||
if err := meta.handler.Serve(client); nil != err {
|
if err := meta.handler.Serve(client); nil != err {
|
||||||
|
@ -52,7 +52,7 @@ func (m *RouteMux) Serve(client net.Conn) error {
|
||||||
// ForwardTCP creates and returns a connection to a local handler target.
|
// ForwardTCP creates and returns a connection to a local handler target.
|
||||||
func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Duration) error {
|
func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Duration) error {
|
||||||
// TODO check servername
|
// TODO check servername
|
||||||
m.list = append(m.list, meta{
|
m.routes = append(m.routes, meta{
|
||||||
addr: servername,
|
addr: servername,
|
||||||
terminate: false,
|
terminate: false,
|
||||||
handler: NewForwarder(target, timeout),
|
handler: NewForwarder(target, timeout),
|
||||||
|
@ -63,7 +63,7 @@ func (m *RouteMux) ForwardTCP(servername string, target string, timeout time.Dur
|
||||||
// HandleTCP creates and returns a connection to a local handler target.
|
// HandleTCP creates and returns a connection to a local handler target.
|
||||||
func (m *RouteMux) HandleTCP(servername string, handler Handler) error {
|
func (m *RouteMux) HandleTCP(servername string, handler Handler) error {
|
||||||
// TODO check servername
|
// TODO check servername
|
||||||
m.list = append(m.list, meta{
|
m.routes = append(m.routes, meta{
|
||||||
addr: servername,
|
addr: servername,
|
||||||
terminate: false,
|
terminate: false,
|
||||||
handler: handler,
|
handler: handler,
|
||||||
|
@ -74,7 +74,7 @@ func (m *RouteMux) HandleTCP(servername string, handler Handler) error {
|
||||||
// HandleTLS creates and returns a connection to a local handler target.
|
// HandleTLS creates and returns a connection to a local handler target.
|
||||||
func (m *RouteMux) HandleTLS(servername string, acme *ACME, handler Handler) error {
|
func (m *RouteMux) HandleTLS(servername string, acme *ACME, handler Handler) error {
|
||||||
// TODO check servername
|
// TODO check servername
|
||||||
m.list = append(m.list, meta{
|
m.routes = append(m.routes, meta{
|
||||||
addr: servername,
|
addr: servername,
|
||||||
terminate: true,
|
terminate: true,
|
||||||
handler: HandlerFunc(func(client net.Conn) error {
|
handler: HandlerFunc(func(client net.Conn) error {
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
package telebit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Server struct {
|
|
||||||
ctx context.Context
|
|
||||||
newConns chan *Conn
|
|
||||||
data []byte
|
|
||||||
dataReady chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Accept() (*Conn, error) {
|
|
||||||
select {
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return nil, errors.New("TODO: ErrClosed")
|
|
||||||
case conn := <-s.newConns:
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read packs transforms local responses into wrapped data for the tunnel
|
|
||||||
func (s *Server) Read(b []byte) (int, error) {
|
|
||||||
select {
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return 0, errors.New("TODO: EOF / ErrClosed")
|
|
||||||
case <-s.dataReady:
|
|
||||||
if 0 == len(s.data) {
|
|
||||||
return s.Read(b)
|
|
||||||
}
|
|
||||||
return s.read(b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) read(b []byte) (int, error) {
|
|
||||||
// TODO mutex data while reading, against writing?
|
|
||||||
|
|
||||||
c := len(b) // capacity
|
|
||||||
a := len(s.data) // available
|
|
||||||
n := c
|
|
||||||
|
|
||||||
// see if the available data is smaller than the receiving buffer
|
|
||||||
if a < c {
|
|
||||||
n = a
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy available data up to capacity
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
b[i] = s.data[i]
|
|
||||||
}
|
|
||||||
// shrink the data slice by amount read
|
|
||||||
s.data = s.data[n:]
|
|
||||||
|
|
||||||
// if there's data left over, flag as ready to read again
|
|
||||||
// otherwise... flag as ready to write?
|
|
||||||
if len(b) > 0 {
|
|
||||||
s.dataReady <- struct{}{}
|
|
||||||
} else {
|
|
||||||
//p.writeReady <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note a read error should not be possible here
|
|
||||||
// as all traffic (including errors) can be wrapped
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close (TODO) should politely close all connections, if possible (set Read() to io.EOF, or use ErrClosed?)
|
|
||||||
func (s *Server) Close() error {
|
|
||||||
return errors.New("not implemented")
|
|
||||||
}
|
|
|
@ -7,6 +7,8 @@ git.rootprojects.org/root/go-gitver/gitver
|
||||||
github.com/caddyserver/certmagic
|
github.com/caddyserver/certmagic
|
||||||
# github.com/cenkalti/backoff/v4 v4.0.0
|
# github.com/cenkalti/backoff/v4 v4.0.0
|
||||||
github.com/cenkalti/backoff/v4
|
github.com/cenkalti/backoff/v4
|
||||||
|
# github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/davecgh/go-spew/spew
|
||||||
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
## explicit
|
## explicit
|
||||||
github.com/dgrijalva/jwt-go
|
github.com/dgrijalva/jwt-go
|
||||||
|
@ -27,6 +29,7 @@ github.com/go-acme/lego/v3/challenge/tlsalpn01
|
||||||
github.com/go-acme/lego/v3/lego
|
github.com/go-acme/lego/v3/lego
|
||||||
github.com/go-acme/lego/v3/log
|
github.com/go-acme/lego/v3/log
|
||||||
github.com/go-acme/lego/v3/platform/config/env
|
github.com/go-acme/lego/v3/platform/config/env
|
||||||
|
github.com/go-acme/lego/v3/platform/tester
|
||||||
github.com/go-acme/lego/v3/platform/wait
|
github.com/go-acme/lego/v3/platform/wait
|
||||||
github.com/go-acme/lego/v3/providers/dns/duckdns
|
github.com/go-acme/lego/v3/providers/dns/duckdns
|
||||||
github.com/go-acme/lego/v3/providers/dns/godaddy
|
github.com/go-acme/lego/v3/providers/dns/godaddy
|
||||||
|
@ -49,6 +52,12 @@ github.com/joho/godotenv/autoload
|
||||||
github.com/klauspost/cpuid
|
github.com/klauspost/cpuid
|
||||||
# github.com/miekg/dns v1.1.27
|
# github.com/miekg/dns v1.1.27
|
||||||
github.com/miekg/dns
|
github.com/miekg/dns
|
||||||
|
# github.com/pmezard/go-difflib v1.0.0
|
||||||
|
github.com/pmezard/go-difflib/difflib
|
||||||
|
# github.com/stretchr/testify v1.5.1
|
||||||
|
## explicit
|
||||||
|
github.com/stretchr/testify/assert
|
||||||
|
github.com/stretchr/testify/require
|
||||||
# golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
# golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073
|
||||||
golang.org/x/crypto/ed25519
|
golang.org/x/crypto/ed25519
|
||||||
golang.org/x/crypto/ed25519/internal/edwards25519
|
golang.org/x/crypto/ed25519/internal/edwards25519
|
||||||
|
@ -76,3 +85,5 @@ gopkg.in/natefinch/lumberjack.v2
|
||||||
gopkg.in/square/go-jose.v2
|
gopkg.in/square/go-jose.v2
|
||||||
gopkg.in/square/go-jose.v2/cipher
|
gopkg.in/square/go-jose.v2/cipher
|
||||||
gopkg.in/square/go-jose.v2/json
|
gopkg.in/square/go-jose.v2/json
|
||||||
|
# gopkg.in/yaml.v2 v2.2.8
|
||||||
|
gopkg.in/yaml.v2
|
||||||
|
|
Loading…
Reference in New Issue