package main import ( "crypto/md5" "encoding/hex" "encoding/json" "github.com/caarlos0/env/v6" "log" "meow.tf/go/cacheinterface" "meow.tf/go/linkinfo" "net" "net/http" "os" "time" ) var ( api *linkinfo.LinkInfoApi cacheInterface cache.CacheInterface ) type apiConfig struct { Cache string `env:"CACHE"` LocalAddress string `env:"LOCAL_ADDRESS"` ImgurClientId string `env:"IMGUR_CLIENT_ID"` YoutubeKey string `env:"YOUTUBE_KEY"` TwitterConsumerKey string `env:"TWITTER_CONSUMER_KEY"` TwitterConsumerSecret string `env:"TWITTER_CONSUMER_SECRET"` TwitterToken string `env:"TWITTER_TOKEN"` TwitterTokenSecret string `env:"TWITTER_TOKEN_SECRET"` } func main() { config := &apiConfig{} if err := env.Parse(config); err != nil { log.Fatalln("Unable to load config from env") } opts := make([]linkinfo.Option, 0) if config.ImgurClientId != "" { opts = append(opts, &linkinfo.ImgurOptions{ ClientId: config.ImgurClientId, }) } if config.YoutubeKey != "" { opts = append(opts, &linkinfo.YoutubeOptions{ Key: config.YoutubeKey, }) } if config.TwitterConsumerKey != "" { opts = append(opts, &linkinfo.TwitterOptions{ ConsumerKey: config.TwitterConsumerKey, ConsumerSecret: config.TwitterConsumerSecret, Token: config.TwitterToken, TokenSecret: config.TwitterTokenSecret, }) } opts = append(opts, &linkinfo.HttpOptions{ UserAgent: "LinkInfo/1.0", }) api = linkinfo.New(opts...) var err error if addr := os.Getenv("LOCAL_ADDRESS"); addr != "" { api.Client, err = constructBoundClient(addr) if err != nil { log.Fatalln("Unable to bind client to address:", err) } } else { api.Client = &http.Client{ Timeout: 15 * time.Second, } } if config.Cache != "" { cacheInterface, err = cache.New(config.Cache) if err != nil { log.Fatalln("Unable to create cache:", err) } } mux := http.NewServeMux() mux.HandleFunc("/info", handleInfoRequest) http.ListenAndServe(":8080", mux) } func handleInfoRequest(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() urlStr := query.Get("url") if urlStr == "" { w.WriteHeader(http.StatusBadRequest) return } h := md5.New() key := hex.EncodeToString(h.Sum([]byte(urlStr))) if cacheInterface != nil { cached, err := cacheInterface.Get(key) if cached != nil && err == nil { w.Header().Set("Content-Type", "application/json") w.Write(cached) return } } ret, err := api.Retrieve(urlStr) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error())) return } b, err := json.Marshal(ret) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if cacheInterface != nil { cacheInterface.Set(key, b, 10*time.Minute) } w.Header().Set("Content-Type", "application/json") w.Write(b) } func constructBoundClient(addr string) (*http.Client, error) { localAddr, err := net.ResolveIPAddr("ip", addr) if err != nil { return nil, err } // You also need to do this to make it work and not give you a // "mismatched local address type ip" // This will make the ResolveIPAddr a TCPAddr without needing to // say what SRC port number to use. localTCPAddr := net.TCPAddr{ IP: localAddr.IP, } dialer := &net.Dialer{ LocalAddr: &localTCPAddr, Timeout: 15 * time.Second, KeepAlive: 15 * time.Second, } return &http.Client{ Transport: &http.Transport{ DialContext: dialer.DialContext, }, }, nil }