Finish up initial mvp

master
Tyler Sommer 2019-03-07 16:17:33 -07:00
parent b648598bba
commit 5abb3b7437
Signed by: tyler-sommer
GPG Key ID: C09C010500DBD008
4 changed files with 199 additions and 41 deletions

76
client.go Normal file
View File

@ -0,0 +1,76 @@
package eokvin // import "github.com/veonik/eokvin"
import (
"crypto/tls"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/url"
)
type ShortURL struct {
url.URL
Original *url.URL
}
type Client struct {
http *http.Client
Endpoint string
Token string
}
func NewClient(token string) *Client {
return &Client{
http: &http.Client{},
Endpoint: "https://eok.vin/new",
Token: token,
}
}
func NewInsecureClient(token string) *Client {
return &Client{
http: &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}},
Endpoint: "https://localhost:3000/new",
Token: token,
}
}
func (c *Client) NewShortURLString(lu string) (*ShortURL, error) {
ou, err := url.Parse(lu)
if err != nil {
return nil, err
}
return c.NewShortURL(ou)
}
func (c *Client) NewShortURL(ou *url.URL) (*ShortURL, error) {
resp, err := c.http.PostForm(
c.Endpoint,
url.Values{"token": {c.Token}, "url": {ou.String()}})
if err != nil {
return nil, err
}
defer resp.Body.Close()
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
r := struct {
Error string `json:"error"`
ShortURL string `json:"short-url"`
}{}
if err := json.Unmarshal(b, &r); err != nil {
return nil, err
}
if r.Error != "" {
return nil, errors.New(r.Error)
}
u, err := url.Parse(r.ShortURL)
if err != nil {
return nil, err
}
return &ShortURL{URL: *u, Original: ou}, nil
}

48
cmd/eokify/main.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"flag"
"fmt"
"os"
"log"
"github.com/veonik/eokvin"
)
var token string
var endpoint string
var insecure bool
func main() {
flag.StringVar(&token, "token", "", "Secret token, required")
flag.StringVar(&endpoint, "endpoint", "https://eok.vin/new", "URL for the running eokvin server")
flag.BoolVar(&insecure, "insecure", false,"If enabled, allows endpoint to be insecure")
flag.Parse()
if token == "" {
log.Fatal("token cannot be blank")
}
if endpoint == "" {
log.Fatal("endpoint cannot be blank")
}
args := flag.Args()
if len(args) != 1 {
log.Fatalf("expected 1 argument, not %d", len(os.Args))
}
u := args[0]
var c *eokvin.Client
if !insecure {
c = eokvin.NewClient(token)
} else {
c = eokvin.NewInsecureClient(token)
}
c.Endpoint = endpoint
s, err := c.NewShortURLString(u)
if err != nil {
log.Fatalf("error creating short URL: %s", err.Error())
}
fmt.Println(s.String())
}

View File

@ -3,16 +3,16 @@ package main
import (
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"encoding/json"
"fmt"
"golang.org/x/crypto/acme/autocert"
"log"
"net/http"
"strings"
"golang.org/x/crypto/acme/autocert"
)
func listenAndServe() error {
func listenAndServeRedirect() error {
l := fmt.Sprintf("%s:%d", listenHost, listenPortHTTP)
srv := &http.Server{Addr: l, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
redirectToCanonicalHost(w, r)
@ -31,7 +31,7 @@ func listenAndServeTLS() error {
HostPolicy: autocert.HostWhitelist(hosts...),
Cache: autocert.DirCache("certs"),
}
srv.TLSConfig = &tls.Config{GetCertificate: m.GetCertificate}
srv.TLSConfig = m.TLSConfig()
}
return srv.ListenAndServeTLS(tlsCertFile, tlsKeyFile)
}
@ -100,7 +100,7 @@ func redirectToCanonicalHost(w http.ResponseWriter, r *http.Request) {
}
// newHandler is an http.Handler that creates a new item in the urlStore store.
var newHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var newHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
link := r.PostFormValue("url")
if len(link) == 0 {
w.WriteHeader(http.StatusBadRequest)
@ -114,7 +114,7 @@ var newHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *ht
urlStore.mu.Lock()
urlStore.entries[k] = newItem(link)
urlStore.mu.Unlock()
b, err := json.Marshal(map[string]string{"short-url": canonicalHost + k.String()})
b, err := json.Marshal(map[string]string{"short-url": canonicalHost + "/" + k.String()})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, err = fmt.Fprintf(w, `{"error": "%s"}\n`, strings.Replace(err.Error(), `"`, `'`, -1))
@ -128,11 +128,11 @@ var newHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *ht
log.Println("error writing response:", err.Error())
}
return
})
}
// indexHandler is a catch-all http.Handler that attempts to lookup items in
// the store based on request path.
var indexHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var indexHandler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
key := strings.TrimLeft(r.URL.Path, "/")
if len(key) == 0 {
w.WriteHeader(http.StatusBadRequest)
@ -157,4 +157,4 @@ var indexHandler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *
}
w.WriteHeader(http.StatusNotFound)
return
})
}

View File

@ -1,11 +1,20 @@
// Command eokvin is a private, single-user, self-hosted URL shortening server.
//
// Short URLs expire after a time and are created by way of HTTP request
// with a static secret token required for authentication. Upon creation, the
// requester will receive the newly generated short URL which can be freely
// accessed by anyone with the link until it expires.
package main
import (
"crypto/sha256"
"errors"
"flag"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
)
@ -15,75 +24,100 @@ var listenPortHTTPS int
var listenPortHTTP int
var canonicalHost string
var tokenSHA256 string
var urlTTL time.Duration
var tlsKeyFile string
var tlsCertFile string
var rawToken string
// urlTTL contains the time.Duration describing how long a short URL shall
// remain accessible.
const urlTTL = 10 * time.Minute
// ShortURLTTL the duration a short URL shall remain accessible.
const ShortURLTTL = 30 * time.Minute
// urlStore contains URL values that map to short, random string keys.
var urlStore = &store{entries: make(map[itemID]item), ttl: urlTTL}
var urlStore = &store{entries: make(map[itemID]item), ttl: ShortURLTTL}
func init() {
flag.StringVar(&listenHost, "host", "eok.vin", "Listen hostname")
flag.IntVar(&listenPortHTTPS, "port", 443, "HTTPS listen port")
flag.IntVar(&listenPortHTTP, "http-port", 80, "HTTP listen port")
flag.DurationVar(&urlTTL, "url-ttl", 30, "Short URLs expire after this delay")
flag.StringVar(&tlsKeyFile, "key-file", "", "TLS private key file, blank for autocert")
flag.StringVar(&tlsCertFile, "cert-file", "", "TLS certificate chain file, blank for autocert")
flag.StringVar(&tokenSHA256, "token", "", "SHA256 of the secret token, used to authenticate")
flag.StringVar(&rawToken, "hash-token", "", "If given, the sha256 of the given value will be printed")
}
func validate() {
flag.Parse()
if len(rawToken) > 0 {
fmt.Printf("%x\n", sha256.Sum256([]byte(rawToken)))
os.Exit(0)
return
}
if len(listenHost) == 0 {
log.Fatal("host cannot be blank")
}
if listenPortHTTPS <= 0 {
log.Fatal("port must be > 0")
}
if listenPortHTTP <= 0 {
log.Fatal("http-port must be > 0")
}
if len(tokenSHA256) != 64 {
log.Fatal("token must be a valid sha256 sum")
}
canonicalHost = fmt.Sprintf("https://%s:%d/", listenHost, listenPortHTTPS)
flag.StringVar(&rawToken, "hash-token", "", "If given, the sha256 of the value will be printed")
}
func main() {
validate()
// Read and validate configuration.
if err := parseFlags(); err != nil {
log.Fatal(err)
}
// Launch the HTTP redirect server.
// Launch the HTTP->HTTPS redirect server.
go func() {
if err := listenAndServe(); err != nil {
log.Println("starting HTTP")
if err := listenAndServeRedirect(); err != nil {
log.Println(err.Error())
}
}()
// Launch the HTTPS main server.
go func() {
log.Println("starting TLS")
if err := listenAndServeTLS(); err != nil {
log.Println(err.Error())
}
}()
// Launch the expired entry reaper.
go func() {
log.Println("starting expired item reaper")
if err := urlStore.expiredItemReaper(); err != nil {
log.Println(err.Error())
}
}()
log.Println("started all goroutines")
// Yield forever.
select {}
}
// parseFlags reads command line options and ensures the program state is
// configured properly, returning an error if it is not.
func parseFlags() error {
flag.Parse()
if len(rawToken) > 0 {
fmt.Printf("%x\n", sha256.Sum256([]byte(rawToken)))
os.Exit(0)
return nil
}
if len(listenHost) == 0 {
return errors.New("host cannot be blank")
}
if listenPortHTTPS <= 0 {
return errors.New("port must be > 0")
}
if listenPortHTTP <= 0 {
return errors.New("http-port must be > 0")
}
if len(tokenSHA256) != 64 {
return errors.New("token must be a valid sha256 sum")
}
parts := []string{"https://", listenHost}
if listenPortHTTPS != 443 {
parts = append(parts, ":", strconv.Itoa(listenPortHTTPS))
}
canonicalHost = fmt.Sprintf("%s", strings.Join(parts, ""))
log.Println("listen host:", listenHost)
log.Println("listen tls port:", listenPortHTTPS)
log.Println("listen http port:", listenPortHTTP)
log.Println("canonical host:", canonicalHost)
log.Println("url ttl:", urlTTL)
log.Println("token:", tokenSHA256)
return nil
}