diff --git a/config.go b/config.go index 4802cce..47ef730 100644 --- a/config.go +++ b/config.go @@ -2,10 +2,8 @@ package main import ( "errors" + "github.com/caarlos0/env/v6" "log" - "os" - "strconv" - "strings" ) var ( @@ -13,9 +11,11 @@ var ( IgnoredWebmentionUrls []string SyndicationTargets []SyndicationTarget SelectedStorage Storage + SelectedMediaStorage MediaStorage SelectedCdn Cdn SelectedSocials Socials SelectedNotificationServices NotificationServices + MediaEndpointUrl string ) type SyndicationTarget struct { @@ -23,80 +23,106 @@ type SyndicationTarget struct { Name string `json:"name"` } -func initConfig() { +type config struct { + BlogUrl string `env:"BLOG_URL,required"` + BaseUrl string `env:"BASE_URL,required"` + MediaUrl string `env:"MEDIA_URL"` + GiteaEndpoint string `env:"GITEA_ENDPOINT"` + GiteaToken string `env:"GITEA_TOKEN"` + BunnyCdnKey string `env:"BUNNY_CDN_KEY"` + BunnyCdnStorageKey string `env:"BUNNY_CDN_STORAGE_KEY"` + BunnyCdnStorageName string `env:"BUNNY_CDN_STORAGE_NAME"` + MicroblogUrl string `env:"MICROBLOG_URL"` + MicroblogToken string `env:"MICROBLOG_TOKEN"` + TelegramUserId int64 `env:"TELEGRAM_USER_ID"` + TelegramBotToken string `env:"TELEGRAM_BOT_TOKEN"` + IgnoredWebmentionUrls []string `env:"WEBMENTION_IGNORED" envSeparator:","` + SyndicationTargets []string `env:"SYNDICATION" envSeparator:","` +} + +func initConfig() (err error) { + cfg := config{} + if err := env.Parse(&cfg); err != nil { + return errors.New("failed to parse config, probably not all required env vars set") + } // Blog URL (required) - blogUrl, err := blogUrl() - if err != nil { - log.Fatal(err) - } - BlogUrl = blogUrl + BlogUrl = cfg.BlogUrl + // Media endpoint + MediaEndpointUrl = cfg.BaseUrl + "/media" // Ignored Webmention URLs (optional) - ignoredWebmentionUrls, err := ignoredWebmentionUrls() - if err != nil { - log.Println(err) - } - IgnoredWebmentionUrls = ignoredWebmentionUrls + IgnoredWebmentionUrls = cfg.IgnoredWebmentionUrls // Syndication Targets (optional) - syndicationTargets, err := syndicationTargets() - if err != nil { - log.Println(err) + targets := make([]SyndicationTarget, 0) + for _, url := range cfg.SyndicationTargets { + targets = append(targets, SyndicationTarget{ + Uid: url, + Name: url, + }) } - SyndicationTargets = syndicationTargets + SyndicationTargets = targets // Find selected storage SelectedStorage = func() Storage { // Gitea - giteaEndpoint, err1 := giteaEndpoint() - giteaToken, err2 := giteaToken() - if err1 == nil && err2 == nil { + if len(cfg.GiteaEndpoint) > 0 && len(cfg.GiteaToken) >= 0 { return &Gitea{ - endpoint: giteaEndpoint, - token: giteaToken, + endpoint: cfg.GiteaEndpoint, + token: cfg.GiteaToken, } } return nil }() if SelectedStorage == nil { - log.Fatal("No storage configured") + return errors.New("no storage configured") + } + // Find selected media storage + SelectedMediaStorage = func() MediaStorage { + // BunnyCDN + if len(cfg.BunnyCdnStorageKey) > 0 && len(cfg.BunnyCdnStorageName) > 0 && len(cfg.MediaUrl) > 0 { + return &BunnyCdnStorage{ + key: cfg.BunnyCdnStorageKey, + storageZoneName: cfg.BunnyCdnStorageName, + baseLocation: cfg.MediaUrl, + } + } + return nil + }() + if SelectedMediaStorage == nil { + return errors.New("no media storage configured") } // Find selected CDN (optional) SelectedCdn = func() Cdn { // BunnyCDN (optional) - bunnyCdnKey, err := bunnyCdnKey() - if err == nil { - return &BunnyCdn{key: bunnyCdnKey} + if len(cfg.BunnyCdnKey) > 0 { + return &BunnyCdn{key: cfg.BunnyCdnKey} } return nil }() if SelectedCdn == nil { - log.Println("No CDN configured") + log.Println("no CDN configured") } // Find configured social networks (optional) SelectedSocials = func() Socials { var socials []Social = nil // Microblog.pub - microblogUrl, err1 := microblogUrl() - microblogToken, err2 := microblogToken() - if err1 == nil && err2 == nil { + if len(cfg.MicroblogUrl) > 0 && len(cfg.MicroblogToken) > 0 { socials = append(socials, &MicroblogPub{ - url: microblogUrl, - token: microblogToken, + url: cfg.MicroblogUrl, + token: cfg.MicroblogToken, }) } return socials }() if SelectedSocials == nil { - log.Println("No social networks configured") + log.Println("no social networks configured") } // Find configured notification services (optional) SelectedNotificationServices = func() NotificationServices { var notificationServices []NotificationService = nil // Telegram - telegramUserId, err1 := telegramUserId() - telegramBotToken, err2 := telegramBotToken() - if err1 == nil && err2 == nil { + if cfg.TelegramUserId > 0 && len(cfg.TelegramBotToken) > 0 { notificationServices = append(notificationServices, &Telegram{ - userId: telegramUserId, - botToken: telegramBotToken, + userId: cfg.TelegramUserId, + botToken: cfg.TelegramBotToken, }) } return notificationServices @@ -104,92 +130,5 @@ func initConfig() { if SelectedNotificationServices == nil { log.Println("No notification services configured") } -} - -func giteaEndpoint() (string, error) { - giteaEndpoint := os.Getenv("GITEA_ENDPOINT") - if len(giteaEndpoint) == 0 || giteaEndpoint == "" { - return "", errors.New("GITEA_ENDPOINT not specified") - } - return giteaEndpoint, nil -} - -func giteaToken() (string, error) { - giteaToken := os.Getenv("GITEA_TOKEN") - if len(giteaToken) == 0 || giteaToken == "" { - return "", errors.New("GITEA_TOKEN not specified") - } - return giteaToken, nil -} - -func blogUrl() (string, error) { - blogURL := os.Getenv("BLOG_URL") - if len(blogURL) == 0 || blogURL == "" { - return "", errors.New("BLOG_URL not specified") - } - return blogURL, nil -} - -func bunnyCdnKey() (string, error) { - bunnyCDNKey := os.Getenv("BUNNY_CDN_KEY") - if len(bunnyCDNKey) == 0 || bunnyCDNKey == "" { - return "", errors.New("BUNNY_CDN_KEY not specified, BunnyCDN features are deactivated") - } - return bunnyCDNKey, nil -} - -func microblogUrl() (string, error) { - microblogUrl := os.Getenv("MICROBLOG_URL") - if len(microblogUrl) == 0 || microblogUrl == "" { - return "", errors.New("MICROBLOG_URL not specified, microblog.pub features are deactivated") - } - return microblogUrl, nil -} - -func microblogToken() (string, error) { - microblogToken := os.Getenv("MICROBLOG_TOKEN") - if len(microblogToken) == 0 || microblogToken == "" { - return "", errors.New("MICROBLOG_TOKEN not specified, microblog.pub features are deactivated") - } - return microblogToken, nil -} - -func telegramUserId() (int64, error) { - telegramUserIdString := os.Getenv("TELEGRAM_USER_ID") - telegramUserId, err := strconv.ParseInt(telegramUserIdString, 10, 64) - if err != nil || len(telegramUserIdString) == 0 || telegramUserIdString == "" { - return 0, errors.New("TELEGRAM_USER_ID not specified, Telegram features are deactivated") - } - return telegramUserId, nil -} - -func telegramBotToken() (string, error) { - telegramBotToken := os.Getenv("TELEGRAM_BOT_TOKEN") - if len(telegramBotToken) == 0 || telegramBotToken == "" { - return "", errors.New("TELEGRAM_BOT_TOKEN not specified, Telegram features are deactivated") - } - return telegramBotToken, nil -} - -func ignoredWebmentionUrls() ([]string, error) { - webmentionIgnored := os.Getenv("WEBMENTION_IGNORED") - if len(webmentionIgnored) == 0 { - return make([]string, 0), errors.New("WEBMENTION_IGNORED not set, no URLs are ignored on Webmention sending") - } - return strings.Split(webmentionIgnored, ","), nil -} - -func syndicationTargets() ([]SyndicationTarget, error) { - syndication := os.Getenv("SYNDICATION") - targets := make([]SyndicationTarget, 0) - if len(syndication) == 0 { - return targets, errors.New("SYNDICATION not set, no targets are returned when querying for syndication targets") - } - for _, url := range strings.Split(syndication, ",") { - targets = append(targets, SyndicationTarget{ - Uid: url, - Name: url, - }) - } - return targets, nil + return nil } diff --git a/go.mod b/go.mod index 4604dcd..3d9f1f8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module codeberg.org/jlelse/hugo-micropub go 1.13 require ( + github.com/caarlos0/env/v6 v6.1.0 github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible github.com/technoweenie/multipartstreamer v1.0.1 // indirect gopkg.in/yaml.v2 v2.2.7 diff --git a/go.sum b/go.sum index 8629e4d..2ef7724 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,16 @@ github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/caarlos0/env/v6 v6.1.0 h1:4FbM+HmZA/Q5wdSrH2kj0KQXm7xnhuO8y3TuOTnOvqc= +github.com/caarlos0/env/v6 v6.1.0/go.mod h1:iUA6X3VCAOwDhoqvgKlTGjjwJzQseIJaFYApUqQkt+8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible h1:OT02onvXX618RBcjxeUA4H7d1PSm5Apg4IET72VgVlE= github.com/go-telegram-bot-api/telegram-bot-api v4.6.5-0.20190904012038-b33efeebc785+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= @@ -12,6 +19,7 @@ golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3 h1:czFLhve3vsQetD6JOJ8NZZvGQ golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= willnorris.com/go/webmention v0.0.0-20191104072158-c7fb13569b62 h1:jqC8A1S2/9WjXSOK/Nl2rYwVgxU7DCnZ/zpOTL1BErI= diff --git a/main.go b/main.go index 3f935e8..8153afe 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,16 @@ import ( ) func main() { - initConfig() + err := initConfig() + if err != nil { + log.Fatal(err) + } log.Println("Starting micropub server...") log.Println("Current time: " + time.Now().Format(time.RFC3339)) log.Println("Blog URL: " + BlogUrl) log.Println("Ignored URLs for Webmention: " + strings.Join(IgnoredWebmentionUrls, ", ")) http.HandleFunc("/micropub", HandleMicroPub) + http.HandleFunc("/media", HandleMedia) http.HandleFunc("/webmention", HandleWebmention) log.Fatal(http.ListenAndServe(":5555", nil)) } diff --git a/mediaendpoint.go b/mediaendpoint.go new file mode 100644 index 0000000..9b593ee --- /dev/null +++ b/mediaendpoint.go @@ -0,0 +1,58 @@ +package main + +import ( + "net/http" + "strings" + "time" +) + +func HandleMedia(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + w.WriteHeader(http.StatusMethodNotAllowed) + _, _ = w.Write([]byte("The HTTP method is not allowed, make a POST request")) + return + } + if contentType := r.Header.Get("content-type"); !strings.Contains(contentType, "multipart/form-data") { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte("Wrong Content-Type")) + return + } + err := r.ParseMultipartForm(0) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Failed to parse Multipart")) + return + } + authCode := r.Header.Get("authorization") + if formAuth := r.FormValue("authorization"); len(authCode) == 0 && len(formAuth) > 0 { + authCode = formAuth + } + if CheckAuthorization(authCode) { + file, header, err := r.FormFile("file") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Failed to get file")) + return + } + fileName := generateRandomString(time.Now(), 15) + mimeType := header.Header.Get("content-type") + originalName := header.Filename + if strings.Contains(mimeType, "jpeg") || strings.Contains(originalName, ".jpeg") || strings.Contains(originalName, ".jpg") { + fileName += ".jpg" + } else if strings.Contains(mimeType, "png") || strings.Contains(originalName, ".png") { + fileName += ".png" + } + location, err := SelectedMediaStorage.Upload(fileName, file) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Failed to upload file")) + return + } + w.Header().Add("Location", location) + w.WriteHeader(http.StatusCreated) + } else { + w.WriteHeader(http.StatusForbidden) + _, _ = w.Write([]byte("Forbidden, there was a problem with the provided access token")) + return + } +} diff --git a/mediastorage.go b/mediastorage.go new file mode 100644 index 0000000..b94b528 --- /dev/null +++ b/mediastorage.go @@ -0,0 +1,35 @@ +package main + +import ( + "errors" + "mime/multipart" + "net/http" + "net/url" +) + +type MediaStorage interface { + Upload(fileName string, file multipart.File) (location string, err error) +} + +// BunnyCDN +type BunnyCdnStorage struct { + // Access Key + key string + // Storage zone name + storageZoneName string + // Base location + baseLocation string +} + +var bunnyCdnStorageUrl = "https://storage.bunnycdn.com" + +func (b BunnyCdnStorage) Upload(fileName string, file multipart.File) (location string, err error) { + client := &http.Client{} + req, _ := http.NewRequest("PUT", bunnyCdnStorageUrl+"/"+url.PathEscape(b.storageZoneName)+"/"+url.PathEscape("/"+fileName), file) + req.Header.Add("AccessKey", b.key) + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 201 { + return "", errors.New("failed to upload file to BunnyCDN") + } + return b.baseLocation + fileName, nil +} diff --git a/micropub.go b/micropub.go index f4ec726..4fc146d 100644 --- a/micropub.go +++ b/micropub.go @@ -8,7 +8,8 @@ import ( ) type MicropubConfig struct { - SyndicateTo []SyndicationTarget `json:"syndicate-to,omitempty"` + SyndicateTo []SyndicationTarget `json:"syndicate-to,omitempty"` + MediaEndpoint string `json:"media-endpoint,omitempty"` } func HandleMicroPub(w http.ResponseWriter, r *http.Request) { @@ -18,7 +19,8 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-type", "application/json") w.WriteHeader(http.StatusOK) jsonBytes, err := json.Marshal(&MicropubConfig{ - SyndicateTo: SyndicationTargets, + SyndicateTo: SyndicationTargets, + MediaEndpoint: MediaEndpointUrl, }) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -71,7 +73,10 @@ func HandleMicroPub(w http.ResponseWriter, r *http.Request) { } return } - if CheckAuthorization(entry, r.Header.Get("authorization")) { + if authHeader := r.Header.Get("authorization"); len(entry.token) == 0 && len(authHeader) > 0 { + entry.token = authHeader + } + if CheckAuthorization(entry.token) { location, err := WriteEntry(entry) if err != nil { w.WriteHeader(http.StatusBadRequest) diff --git a/validation.go b/validation.go index 6af1b5c..5556a3d 100644 --- a/validation.go +++ b/validation.go @@ -75,13 +75,8 @@ func checkAccess(token string) (bool, error) { return true, nil } -func CheckAuthorization(entry *Entry, token string) bool { - if len(token) < 1 { // there is no token provided - return false - } else { - entry.token = token - } - if ok, err := checkAccess(entry.token); ok { +func CheckAuthorization(token string) bool { + if ok, err := checkAccess(token); ok { return true } else if err != nil { return false