mirror of https://github.com/veonik/eokvin
Finish up initial mvp
parent
b648598bba
commit
5abb3b7437
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue