diff --git a/cmd/root.go b/cmd/root.go index bf72649c51f60d2af23b144cf2463af94e76d12f..6e4dd57fe1e341c6ba76c62988bfe9403f49043c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,107 +2,39 @@ package cmd import ( "fmt" - "image/png" - "log" - "net/http" "os" - "path" - "text/template" - "github.com/ehedenst/go-ipa/constant" "github.com/joho/godotenv" - "github.com/phinexdaz/ipapk" - "github.com/skip2/go-qrcode" "github.com/spf13/cobra" "github.com/spf13/viper" ) -type AppInfoWithURL struct { - *ipapk.AppInfo - URL string -} - -var scheme string -var file string -var filePath string -var appInfo *ipapk.AppInfo -var manifestTemplate *template.Template - -func iconHandler(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "image/png") - png.Encode(w, appInfo.Icon) -} - -func qrcodeHandler(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "image/png") - url := fmt.Sprintf("itms-services://?action=download-manifest&url=%s://%s/manifest.plist", scheme, req.Host) - image, _ := qrcode.Encode(url, qrcode.Medium, 256) - w.Write(image) -} - -func manifestHandler(w http.ResponseWriter, req *http.Request) { - w.Header().Set("Content-Type", "text/xml") - appInfoWithURL := AppInfoWithURL{ - AppInfo: appInfo, - URL: fmt.Sprintf("%s://%s%s", scheme, req.Host, filePath), - } - manifestTemplate.Execute(w, appInfoWithURL) -} - -func fileDownloadHandler(w http.ResponseWriter, req *http.Request) { - http.ServeFile(w, req, file) -} - var rootCmd = &cobra.Command{ Use: "go-ipa", Short: "Root Short", Long: `Root Long`, - Run: func(cmd *cobra.Command, args []string) { - var err error - appInfo, err = ipapk.NewAppParser(file) - filePath = fmt.Sprintf("/%s", path.Base(file)) - http.HandleFunc("/icon.png", iconHandler) - http.HandleFunc("/qrcode.png", qrcodeHandler) - http.HandleFunc("/manifest.plist", manifestHandler) - http.HandleFunc(filePath, fileDownloadHandler) - addr := viper.GetString("ADDR") - certFile := viper.GetString("CERT") - if certFile != "" { - scheme = "https" - keyFile := viper.GetString("KEY") - err = http.ListenAndServeTLS(addr, certFile, keyFile, nil) - } else { - scheme = "http" - err = http.ListenAndServe(addr, nil) - } - if err != nil { - log.Fatal("ListenAndServe: ", err) - } - }, -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(-1) - } } func init() { cobra.OnInitialize(initEnv) - rootCmd.PersistentFlags().StringP("addr", "a", ":9313", "addr") - rootCmd.PersistentFlags().StringP("cert", "c", "", "cert") - rootCmd.PersistentFlags().StringP("key", "k", "", "key") - rootCmd.Flags().StringVarP(&file, "file", "f", "", "file (required)") - rootCmd.MarkFlagRequired("file") + rootCmd.PersistentFlags().StringP("url", "b", "", "Set a base URL") viper.SetEnvPrefix("IPA") - viper.BindPFlag("ADDR", rootCmd.PersistentFlags().Lookup("addr")) - viper.BindPFlag("CERT", rootCmd.PersistentFlags().Lookup("cert")) - viper.BindPFlag("KEY", rootCmd.PersistentFlags().Lookup("key")) - manifestTemplate, _ = template.New("manifest").Parse(constant.ManifestTemplate) + viper.BindPFlag("BASE_URL", rootCmd.PersistentFlags().Lookup("url")) } func initEnv() { _ = godotenv.Load(".env") viper.AutomaticEnv() } + +func Execute() { + defer func() { + if rec := recover(); rec != nil { + fmt.Println(rec) + os.Exit(-1) + } + }() + if err := rootCmd.Execute(); err != nil { + panic(err) + } +} diff --git a/cmd/share.go b/cmd/share.go new file mode 100644 index 0000000000000000000000000000000000000000..756422b559ddd85723dec21d7859acc49e220175 --- /dev/null +++ b/cmd/share.go @@ -0,0 +1,144 @@ +package cmd + +import ( + "errors" + "fmt" + "image/png" + "net/http" + "os" + "path" + "strings" + "text/template" + + "github.com/ehedenst/go-ipa/constant" + "github.com/mdp/qrterminal" + "github.com/phinexdaz/ipapk" + "github.com/skip2/go-qrcode" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type ExtendedAppInfo struct { + *ipapk.AppInfo + IsIPA bool + Filename string + BaseURL string + DownloadURL string +} + +var useTLS bool +var useHalfBlock bool + +var scheme string +var file string +var appInfo *ipapk.AppInfo + +func getExtendedAppInfo(host string) ExtendedAppInfo { + + fileName := path.Base(file) + isIPA := strings.ToLower(path.Ext(fileName)) == ".ipa" + baseURL := fmt.Sprintf("%s://%s", scheme, host) + if viper.IsSet("BASE_URL") { + baseURL = viper.GetString("BASE_URL") + } + + var downloadURL string + if isIPA { + downloadURL = fmt.Sprintf("itms-services://?action=download-manifest&url=%s/manifest.plist", baseURL) + } else { + downloadURL = fmt.Sprintf("%s/%s", baseURL, fileName) + } + + return ExtendedAppInfo{ + AppInfo: appInfo, + Filename: fileName, + IsIPA: isIPA, + BaseURL: baseURL, + DownloadURL: downloadURL, + } +} + +func iconHandler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "image/png") + png.Encode(w, appInfo.Icon) +} + +func qrcodeHandler(w http.ResponseWriter, req *http.Request) { + appInfo := getExtendedAppInfo(req.Host) + w.Header().Set("Content-Type", "image/png") + image, _ := qrcode.Encode(appInfo.DownloadURL, qrcode.Medium, 256) + w.Write(image) +} + +func indexHandler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/html") + tmpl, _ := template.New("index").Parse(constant.IndexTemplate) + tmpl.Execute(w, getExtendedAppInfo(req.Host)) +} + +func manifestHandler(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/xml") + tmpl, _ := template.New("manifest").Parse(constant.ManifestTemplate) + tmpl.Execute(w, getExtendedAppInfo(req.Host)) +} + +func fileDownloadHandler(w http.ResponseWriter, req *http.Request) { + http.ServeFile(w, req, file) +} + +var shareCmd = &cobra.Command{ + Use: "share [flags] [file]", + Short: "share Short", + Long: `share Long`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return errors.New("requires a file argument") + } + // Todo: check if it is a valid file + return nil + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + + file = args[0] + + http.HandleFunc("/qrcode.png", qrcodeHandler) + http.HandleFunc(fmt.Sprintf("/%s", path.Base(file)), fileDownloadHandler) + + if appInfo, err = ipapk.NewAppParser(file); err == nil { + http.HandleFunc("/", indexHandler) + http.HandleFunc("/index.html", indexHandler) + http.HandleFunc("/icon.png", iconHandler) + http.HandleFunc("/manifest.plist", manifestHandler) + } + + if viper.IsSet("BASE_URL") { + extAppInfo := getExtendedAppInfo("") + fmt.Println(extAppInfo.DownloadURL) + if useHalfBlock { + qrterminal.GenerateHalfBlock(extAppInfo.DownloadURL, qrterminal.L, os.Stdout) + } else { + qrterminal.Generate(extAppInfo.DownloadURL, qrterminal.L, os.Stdout) + } + } + + addr := viper.GetString("ADDR") + if useTLS { + scheme = "https" + return http.ListenAndServeTLS(addr, viper.GetString("CERT"), viper.GetString("KEY"), nil) + } + scheme = "http" + return http.ListenAndServe(addr, nil) + }, +} + +func init() { + rootCmd.AddCommand(shareCmd) + shareCmd.Flags().BoolVar(&useTLS, "ssl", true, "enable SSL/TLS") + shareCmd.Flags().BoolVar(&useHalfBlock, "half", false, "generate a smaller QR code in the terminal") + shareCmd.Flags().StringP("addr", "a", ":8080", "address and port on which the server will accept requests") + shareCmd.Flags().StringP("cert", "c", "", "path to SSL certificate") + shareCmd.Flags().StringP("key", "k", "", "path to SSL secret key") + viper.BindPFlag("ADDR", shareCmd.Flags().Lookup("addr")) + viper.BindPFlag("CERT", shareCmd.Flags().Lookup("cert")) + viper.BindPFlag("KEY", shareCmd.Flags().Lookup("key")) +} diff --git a/constant/templates.go b/constant/templates.go index ea51f0f84ea8b382d24e4788c45f8cbd8cc784c7..42092b25b9fae4e34e2b095a7efd6c5b58bbfc54 100644 --- a/constant/templates.go +++ b/constant/templates.go @@ -1,5 +1,36 @@ package constant +var IndexTemplate = ` +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <meta name="Description" content="Ad-Hoc distribution for {{.Filename}}" /> + + <title>{{.Name}}</title> + + <link rel="icon" type="image/png" href="icon.png" /> + + <style> + h1 {color:red;} + p {color:blue;} + </style> + + </head> + + <body> + <img src="icon.png"> + <img src="qrcode.png"> + <p><a href="{{.DownloadURL}}">{{.Name}}</a></p> + <p>{{.BundleId}}</p> + <p>{{.Version}}</p> + <p>{{.Build}}</p> + <p>{{.Size}}</p> + </body> +</html> +` + var ManifestTemplate = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> @@ -13,7 +44,7 @@ var ManifestTemplate = `<?xml version="1.0" encoding="UTF-8"?> <key>kind</key> <string>software-package</string> <key>url</key> - <string>{{.URL}}</string> + <string>{{.BaseURL}}/{{.Filename}}</string> </dict> </array> <key>metadata</key>