From 0d1be93fef2a14d2b090c965a4b86309cbdd9d5a Mon Sep 17 00:00:00 2001 From: idk Date: Wed, 16 Nov 2022 15:15:45 -0500 Subject: [PATCH] move tbget stuff from i2p.plugins.tor-manager --- get.go | 971 +++++++++++++++++++++++++++++++++++++++++++++++++++++ getffox.go | 275 +++++++++++++++ gpg.go | 40 +++ torrent.go | 325 ++++++++++++++++++ 4 files changed, 1611 insertions(+) create mode 100644 get.go create mode 100644 getffox.go create mode 100644 gpg.go create mode 100644 torrent.go diff --git a/get.go b/get.go new file mode 100644 index 0000000..c4293e3 --- /dev/null +++ b/get.go @@ -0,0 +1,971 @@ +package tbget + +import ( + "archive/tar" + "context" + "embed" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/cloudfoundry/jibber_jabber" + "github.com/cretz/bine/tor" + "github.com/dustin/go-humanize" + sam "github.com/eyedeekay/sam3/helper" + "github.com/itchio/damage" + "github.com/itchio/damage/hdiutil" + "github.com/itchio/headway/state" + "github.com/magisterquis/connectproxy" + cp "github.com/otiai10/copy" + "github.com/ulikunitz/xz" + + "golang.org/x/net/proxy" +) + +// WORKING_DIR is the working directory for the application. +var WORKING_DIR = "" + +// DefaultDir returns the default directory for the application. +func DefaultDir() string { + if WORKING_DIR == "" { + WORKING_DIR, _ = os.Getwd() + } + if !FileExists(WORKING_DIR) { + os.MkdirAll(WORKING_DIR, 0755) + } + wd, err := filepath.Abs(WORKING_DIR) + if err != nil { + log.Fatal(err) + } + return wd +} + +// UNPACK_PATH returns the path to the unpacked files. +func UNPACK_PATH() string { + var UNPACK_PATH = filepath.Join(DefaultDir(), "unpack") + return UNPACK_PATH +} + +// DOWNLOAD_PATH returns the path to the downloads. +func DOWNLOAD_PATH() string { + var DOWNLOAD_PATH = filepath.Join(DefaultDir(), "tor-browser") + return DOWNLOAD_PATH +} + +// TOR_UPDATES_URL is the URL of the Tor Browser update list. +const TOR_UPDATES_URL string = "https://aus1.torproject.org/torbrowser/update_3/release/downloads.json" + +func Languages() []string { + jsonText, err := http.Get(TOR_UPDATES_URL) + if err != nil { + return []string{} + } + defer jsonText.Body.Close() + jsonBytes, err := ioutil.ReadAll(jsonText.Body) + if err != nil { + return []string{} + } + var updates map[string]interface{} + if err := json.Unmarshal(jsonBytes, &updates); err != nil { + return []string{} + } + var languages []string + //updates[] + for i := range updates["downloads"].(map[string]interface{})["win64"].(map[string]interface{}) { + languages = append(languages, i) + } + return languages +} + +var ( + // DefaultIETFLang is the default language for the TBDownloader. + DefaultIETFLang, _ = jibber_jabber.DetectIETF() +) + +// TBDownloader is a struct which manages browser updates +type TBDownloader struct { + UnpackPath string + DownloadPath string + Lang string + OS, ARCH string + Mirror string + Verbose bool + NoUnpack bool + Profile *embed.FS + listener net.Listener +} + +// OS is the operating system of the TBDownloader. +var OS = "linux" + +// ARCH is the architecture of the TBDownloader. +var ARCH = "64" + +// NewTBDownloader returns a new TBDownloader with the given language, using the TBDownloader's OS/ARCH pair +func NewTBDownloader(lang string, os, arch string, content *embed.FS) *TBDownloader { + OS = os + ARCH = arch + return &TBDownloader{ + Lang: lang, + DownloadPath: DOWNLOAD_PATH(), + UnpackPath: UNPACK_PATH(), + OS: os, + ARCH: arch, + Verbose: false, + Profile: content, + } +} + +// ServeHTTP serves the DOWNLOAD_PATH as a mirror +func (t *TBDownloader) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.URL.Path = path.Clean(r.URL.Path) + ext := filepath.Ext(r.URL.Path) + if ext == ".json" { + w.Header().Set("Content-Type", "application/json") + if FileExists(filepath.Join(t.DownloadPath, "mirror.json")) { + http.ServeFile(w, r, filepath.Join(t.DownloadPath, "mirror.json")) + } + } + if FileExists(filepath.Join(t.DownloadPath, r.URL.Path)) { + http.ServeFile(w, r, filepath.Join(t.DownloadPath, r.URL.Path)) + return + } +} + +// Serve runs ServeHTTP on an I2P listener +func (t *TBDownloader) Serve() { + var err error + t.listener, err = sam.I2PListener("torbrowser-mirror", "127.0.0.1:7656", filepath.Join(t.UnpackPath, "torbrowser-mirror")) + if err != nil { + log.Fatal(err) + } + defer t.listener.Close() + http.Serve(t.listener, t) +} + +// GetRuntimePair returns the runtime.GOOS and runtime.GOARCH pair. +func (t *TBDownloader) GetRuntimePair() string { + if t.OS != "" && t.ARCH != "" { + return fmt.Sprintf("%s%s", t.OS, t.ARCH) + } + switch runtime.GOOS { + case "darwin": + t.OS = "osx" + case "linux": + t.OS = "linux" + case "windows": + t.OS = "win" + default: + t.OS = "unknown" + } + switch runtime.GOARCH { + case "amd64": + t.ARCH = "64" + case "386": + t.ARCH = "32" + default: + t.ARCH = "64" + } + if t.OS != "osx" { + return fmt.Sprintf("%s%s", t.OS, t.ARCH) + } + return t.OS +} + +// GetUpdater returns the updater for the given language, using the TBDownloader's OS/ARCH pair +// and only the defaults. It returns the URL of the updater and the detatched signature, or an error if one is not found. +func (t *TBDownloader) GetUpdater() (string, string, error) { + return t.GetUpdaterForLang(t.Lang) +} + +// GetUpdaterForLang returns the updater for the given language, using the TBDownloader's OS/ARCH pair +// it expects ietf to be a language. It returns the URL of the updater and the detatched signature, or an error if one is not found. +func (t *TBDownloader) GetUpdaterForLang(ietf string) (string, string, error) { + jsonText, err := http.Get(TOR_UPDATES_URL) + if err != nil { + return "", "", fmt.Errorf("t.GetUpdaterForLang: %s", err) + } + defer jsonText.Body.Close() + return t.GetUpdaterForLangFromJSON(jsonText.Body, ietf) +} + +// GetUpdaterForLangFromJSON returns the updater for the given language, using the TBDownloader's OS/ARCH pair +// it expects body to be a valid json reader and ietf to be a language. It returns the URL of the updater and +// the detatched signature, or an error if one is not found. +func (t *TBDownloader) GetUpdaterForLangFromJSON(body io.ReadCloser, ietf string) (string, string, error) { + jsonBytes, err := io.ReadAll(body) + if err != nil { + return "", "", fmt.Errorf("t.GetUpdaterForLangFromJSON: %s", err) + } + t.MakeTBDirectory() + if err = ioutil.WriteFile(filepath.Join(t.DownloadPath, "downloads.json"), jsonBytes, 0644); err != nil { + return "", "", fmt.Errorf("t.GetUpdaterForLangFromJSON: %s", err) + } + return t.GetUpdaterForLangFromJSONBytes(jsonBytes, ietf) +} + +// Log logs things if Verbose is true. +func (t *TBDownloader) Log(function, message string) { + if t.Verbose { + log.Println(fmt.Sprintf("%s: %s", function, message)) + } +} + +// MakeTBDirectory creates the tor-browser directory if it doesn't exist. It also unpacks a local copy of the TPO signing key. +func (t *TBDownloader) MakeTBDirectory() { + os.MkdirAll(t.DownloadPath, 0755) + + tpk := "TPO-signing-key.pub" + if t.OS == "linux" && runtime.GOARCH == "arm64" { + tpk = "NOT-TPO-signing-key.pub" + } + + empath := path.Join("tor-browser", tpk) + opath := filepath.Join(t.DownloadPath, tpk) + if !FileExists(opath) { + t.Log("MakeTBDirectory()", "Initial TPO signing key not found, using the one embedded in the executable") + bytes, err := t.Profile.ReadFile(empath) + if err != nil { + log.Fatal(err) + } + t.Log("MakeTBDirectory()", "Writing TPO signing key to disk") + err = ioutil.WriteFile(opath, bytes, 0644) + if err != nil { + log.Fatal(err) + } + t.Log("MakeTBDirectory()", "Writing TPO signing key to disk complete") + } + empath = path.Join("tor-browser", "unpack", "awo@eyedeekay.github.io.xpi") + dpath := filepath.Join(t.DownloadPath, "awo@eyedeekay.github.io.xpi") + opath = filepath.Join(t.UnpackPath, "awo@eyedeekay.github.io.xpi") + if !FileExists(opath) { + t.Log("MakeTBDirectory()", "Initial TAWO XPI not found, using the one embedded in the executable") + bytes, err := t.Profile.ReadFile(empath) + if err != nil { + log.Fatal(err) + } + os.MkdirAll(filepath.Dir(dpath), 0755) + os.MkdirAll(filepath.Dir(opath), 0755) + t.Log("MakeTBDirectory()", "Writing AWO XPI to disk") + err = ioutil.WriteFile(opath, bytes, 0644) + if err != nil { + log.Fatal(err) + } + err = ioutil.WriteFile(dpath, bytes, 0644) + if err != nil { + log.Fatal(err) + } + t.Log("MakeTBDirectory()", "Writing AWO XPI disk complete") + } +} + +// GetUpdaterForLangFromJSONBytes returns the updater for the given language, using the TBDownloader's OS/ARCH pair +// it expects jsonBytes to be a valid json string and ietf to be a language. It returns the URL of the updater and +// the detatched signature, or an error if one is not found. +func (t *TBDownloader) GetUpdaterForLangFromJSONBytes(jsonBytes []byte, ietf string) (string, string, error) { + t.MakeTBDirectory() + var dat map[string]interface{} + t.Log("GetUpdaterForLangFromJSONBytes()", "Parsing JSON") + if err := json.Unmarshal(jsonBytes, &dat); err != nil { + return "", "", fmt.Errorf("func (t *TBDownloader)Name: %s", err) + } + t.Log("GetUpdaterForLangFromJSONBytes()", "Parsing JSON complete") + if platform, ok := dat["downloads"]; ok { + rtp := t.GetRuntimePair() + if updater, ok := platform.(map[string]interface{})[rtp]; ok { + if langUpdater, ok := updater.(map[string]interface{})[ietf]; ok { + t.Log("GetUpdaterForLangFromJSONBytes()", "Found updater for language") + bin := langUpdater.(map[string]interface{})["binary"].(string) + sig := langUpdater.(map[string]interface{})["sig"].(string) + return t.MirrorIze(bin), t.MirrorIze(sig), nil + } + // If we didn't find the language, try splitting at the hyphen + lang := strings.Split(ietf, "-")[0] + if langUpdater, ok := updater.(map[string]interface{})[lang]; ok { + t.Log("GetUpdaterForLangFromJSONBytes()", "Found updater for backup language") + bin := langUpdater.(map[string]interface{})["binary"].(string) + sig := langUpdater.(map[string]interface{})["sig"].(string) + return t.MirrorIze(bin), t.MirrorIze(sig), nil + } + // If we didn't find the language after splitting at the hyphen, try the default + t.Log("GetUpdaterForLangFromJSONBytes()", "Last attempt, trying default language") + return t.GetUpdaterForLangFromJSONBytes(jsonBytes, t.Lang) + } + return "", "", fmt.Errorf("t.GetUpdaterForLangFromJSONBytes: no updater for platform %s", rtp) + } + return "", "", fmt.Errorf("t.GetUpdaterForLangFromJSONBytes: %s", ietf) +} + +func (t *TBDownloader) MirrorIze(replaceStr string) string { + log.Println("MirrorIze()", "Replacing", replaceStr, t.Mirror) + if t.OS == "linux" && runtime.GOARCH == "arm64" { + replaceStr = strings.Replace(replaceStr, "linux64", "linux-arm64", -1) + if strings.HasSuffix(replaceStr, ".tar.xz.asc") { + //sha256sums-unsigned-build.txt.asc + lastElement := filepath.Base( + strings.Replace(replaceStr, "https://", strings.Replace(replaceStr, "http://", "", 1), 1), + ) + replaceStr = strings.Replace(replaceStr, lastElement, "sha256sums-unsigned-build.txt.asc", -1) + } + } + if strings.Contains(t.Mirror, "i2psnark") { + replaceStr = strings.Replace(replaceStr, "https://dist.torproject.org/torbrowser/", t.Mirror, 1) + dpath := filepath.Base(replaceStr) + replaceStr = strings.Replace(replaceStr, "http://", "", 1) + replaceStr = filepath.Dir(replaceStr) + replaceStr = filepath.Dir(replaceStr) + newurl := "http://" + filepath.Join(replaceStr, dpath) + log.Println("MirrorIze()", "Final URL", newurl) + return newurl + } + if t.Mirror != "" { + return strings.Replace(replaceStr, "https://dist.torproject.org/torbrowser/", t.Mirror, 1) + } + log.Println("MirrorIze()", "Final URL", replaceStr) + return replaceStr +} + +type WriteCounter struct { + Total uint64 +} + +func (wc *WriteCounter) Write(p []byte) (int, error) { + n := len(p) + wc.Total += uint64(n) + wc.PrintProgress() + return n, nil +} + +func (wc WriteCounter) PrintProgress() { + fmt.Fprintf(os.Stderr, "\r%s", strings.Repeat(" ", 35)) + fmt.Fprintf(os.Stderr, "\rDownloading... %s complete", humanize.Bytes(wc.Total)) +} + +func (t *TBDownloader) StartConf() *tor.StartConf { + return StartConf(t.TorPath()) +} + +func StartConf(tp string) *tor.StartConf { + paths := []string{ + "/bin/tor", + "/usr/bin/tor", + "/usr/sbin/tor", + "/usr/local/bin/tor", + "/usr/bin/tor", + } + path := strings.Split(os.Getenv("PATH"), ":") + for _, p := range path { + p := filepath.Join(p, "tor") + paths = append(paths, p) + } + for _, path := range paths { + if FileExists(path) { + return &tor.StartConf{ + ExePath: path, + RetainTempDataDir: false, + } + } + } + if FileExists(tp) { + return &tor.StartConf{ + ExePath: tp, + RetainTempDataDir: false, + } + } + return nil +} + +// SetupProxy sets up the proxy for the given URL +func (t *TBDownloader) SetupProxy() error { + return SetupProxy(t.Mirror, t.TorPath()) +} + +func unSetupProxy() { + http.DefaultClient.Transport = nil +} + +var t *tor.Tor + +func SetupProxy(mirror, tp string) error { + var d proxy.Dialer + http.DefaultClient.Transport = nil + defer unSetupProxy() + if MirrorIsI2P(mirror) { + log.Println("Using I2P mirror, setting up proxy") + var err error + proxyURL, err := url.Parse("http://127.0.0.1:4444") + if err != nil { + return err + } + d, err = connectproxy.New(proxyURL, proxy.Direct) + if nil != err { + return err + } + tr := &http.Transport{ + Dial: d.Dial, + } + http.DefaultClient.Transport = tr + } else { + nut := os.Getenv("TOR_MANAGER_NEVER_USE_TOR") + if nut != "true" { + if !strings.Contains(mirror, "127.0.0.1") && !strings.Contains(mirror, "localhost") { + if tmp, torerr := net.Listen("tcp", "127.0.0.1:9050"); torerr != nil { + log.Println("System Tor is running, downloading over that because obviously.") + is_flatpak := os.Getenv("APP_ID") != "" + if is_flatpak { + log.Println("Flatpak detected, using Tor without bine") + url_i := url.URL{} + url_proxy, err := url_i.Parse("socks5://127.0.0.1:9050") + if err != nil { + return err + } + + tr := &http.Transport{} + tr.Proxy = http.ProxyURL(url_proxy) // set proxy + http.DefaultClient.Transport = tr + return nil + } + var err error + if t == nil { + t, err = tor.Start(context.Background(), StartConf(tp)) + if err != nil { + if t == nil { + return err + } + } + } + //defer t.Close() + // Wait at most a minute to start network and get + dialCtx, _ := context.WithTimeout(context.Background(), time.Minute) + //defer dialCancel() + // Make connection + dialer, err := t.Dialer(dialCtx, nil) + if err != nil { + return err + } + tr := &http.Transport{DialContext: dialer.DialContext} + http.DefaultClient.Transport = tr + } else { + tmp.Close() + } + } + } + } + return nil +} + +// SingleFileDownload downloads a single file from the given URL to the given path. +// it returns the path to the downloaded file, or an error if one is encountered. +func (t *TBDownloader) SingleFileDownload(dl, name string, rangebottom int64) (string, error) { + t.MakeTBDirectory() + path := filepath.Join(t.DownloadPath, name) + if filepath.IsAbs(name) { + path = name + } + + t.Log("SingleFileDownload()", fmt.Sprintf("Checking for updates %s to %s", dl, path)) + if !t.BotherToDownload(dl, name) { + t.Log("SingleFileDownload()", "File already exists, skipping download") + return path, nil + } + err := t.SetupProxy() + if err != nil { + return "", err + } + dlurl, err := url.Parse(dl) + if err != nil { + return "", err + } + if FileExists(path) { + size, err := os.Stat(path) + if err != nil { + return "", err + } + rangebottom = size.Size() + t.Log("SingleFileDownload()", fmt.Sprintf("Resuming download from %d", rangebottom)) + } + req := http.Request{ + Method: "GET", + URL: dlurl, + Header: http.Header{ + "Range": []string{fmt.Sprintf("bytes=%d-", rangebottom)}, + }, + } + t.Log("SingleFileDownload()", "Downloading file "+dl) + //file, err := http.Get(dl) + file, err := http.DefaultClient.Do(&req) + //Do(&req, nil) + if err != nil { + return "", fmt.Errorf("SingleFileDownload: Request Error %s", err) + } + defer file.Body.Close() + outFile, err := Create(path) + if err != nil { + return "", fmt.Errorf("SingleFileDownload: Write Error %s", err) + } + defer outFile.Close() + // Create our progress reporter and pass it to be used alongside our writer + counter := &WriteCounter{ + Total: uint64(rangebottom), + } + if rangebottom, err := io.Copy(outFile, io.TeeReader(file.Body, counter)); err != nil { + return t.SingleFileDownload(dl, name, rangebottom) + //"", err + } + + // The progress use the same line so print a new line once it's finished downloading + fmt.Print("\n") + //io.Copy(outFile, file.Body) + t.Log("SingleFileDownload()", "Downloading file complete") + return path, nil +} + +func Create(path string) (*os.File, error) { + if FileExists(path) { + stat, err := os.Stat(path) + if err != nil { + return nil, err + } + return os.OpenFile(path, os.O_APPEND|os.O_WRONLY, stat.Mode().Perm()) + } + // Create the file + outFile, err := os.Create(path) + if err != nil { + return nil, err + } + return outFile, nil +} + +// FileExists returns true if the given file exists. It will return true if used on an existing directory. +func FileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} + +func (t *TBDownloader) FetchContentLength(dl, name string) (int64, error) { + t.MakeTBDirectory() + return FetchContentLength(dl, name) +} +func FetchContentLength(dl, name string) (int64, error) { + log.Println("FetchContentLength():", fmt.Sprintf("Checking for updates %s to %s", dl, name)) + err := SetupProxy(dl, "") + if err != nil { + return 0, err + } + dlurl, err := url.Parse(dl) + if err != nil { + return 0, err + } + req := http.Request{ + Method: "HEAD", + URL: dlurl, + } + log.Println("FetchContentLength()", "Downloading file "+dl) + //file, err := http.Get(dl) + file, err := http.DefaultClient.Do(&req) + //Do(&req, nil) + if err != nil { + return 0, fmt.Errorf("FetchContentLength: Request Error %s", err) + } + file.Body.Close() + log.Println("Content-Length:", file.ContentLength) + return file.ContentLength, nil +} + +// BotherToDownload returns true if we need to download a file because we don't have an up-to-date +// version yet. +func (t *TBDownloader) BotherToDownload(dl, name string) bool { + path := filepath.Join(t.DownloadPath, name) + if !FileExists(path) { + return true + } + stat, err := os.Stat(path) + if err != nil { + return true + } + // 86 MB + if !strings.Contains(name, ".asc") { + contentLength, err := t.FetchContentLength(dl, name) + if err != nil { + return true + } + + l := 4 + if len(strconv.Itoa(int(contentLength))) < 4 { + return true + } + lenString := strconv.Itoa(int(contentLength))[:l] + lenSize := strconv.Itoa(int(stat.Size()))[:l] + fmt.Fprintf(os.Stderr, "comparing sizes: %v %v", lenString, lenSize) + + if stat.Size() == contentLength { + //if lenString != lenSize { + // return true + //} else { + fmt.Fprintf(os.Stderr, "BotherToDownload(): %s is fully downloaded\n", name) + return false + //} + } + } + defer ioutil.WriteFile(filepath.Join(t.DownloadPath, name+".last-url"), []byte(dl), 0644) + lastURL, err := ioutil.ReadFile(filepath.Join(t.DownloadPath, name+".last-url")) + if err != nil { + return true + } + if string(lastURL) == dl { + return false + } + return true +} + +// NamePerPlatform returns the name of the updater for the given platform with appropriate extensions. +func (t *TBDownloader) NamePerPlatform(ietf, version string) string { + extension := "tar.xz" + windowsonly := "" + switch t.OS { + case "osx": + extension = "dmg" + case "win": + windowsonly = "-installer" + extension = "exe" + } + //version, err := t.Get + return fmt.Sprintf("tor-browser%s-%s-%s_%s.%s", windowsonly, t.GetRuntimePair(), version, ietf, extension) +} + +func (t *TBDownloader) GetVersion() string { + binary, _, err := t.GetUpdaterForLang(t.Lang) + if err != nil { + return "" + } + version := strings.Split(binary, "/")[len(strings.Split(binary, "/"))-2] + return version +} + +func (t *TBDownloader) GetName() string { + return t.NamePerPlatform(t.Lang, t.GetVersion()) +} + +// DownloadUpdater downloads the updater for the t.Lang. It returns +// the path to the downloaded updater and the downloaded detatched signature, +// or an error if one is encountered. +func (t *TBDownloader) DownloadUpdater() (string, string, string, error) { + return t.DownloadUpdaterForLang(t.Lang) +} + +// DownloadUpdaterForLang downloads the updater for the given language, overriding +// t.Lang. It returns the path to the downloaded updater and the downloaded +// detatched signature, or an error if one is encountered. +func (t *TBDownloader) DownloadUpdaterForLang(ietf string) (string, string, string, error) { + binary, sig, err := t.GetUpdaterForLang(ietf) + if err != nil { + return "", "", "", fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + version := t.GetVersion() + if strings.Contains(t.Mirror, "i2psnark") { + if !TorrentDownloaded(ietf, t.GetRuntimePair()) { + t.Log("DownloadUpdaterForLang()", "Downloading torrent") + SetupProxy("http://idk.i2p/", "") + //Download the torrent files from their static locations. + i2psnark, err := FindSnarkDirectory() + if err != nil { + return "", "", "", err + } + log.Println("Downloading torrent from", i2psnark) + asctorrent := filepath.Join(t.NamePerPlatform(ietf, version) + ".asc" + ".torrent") + fmt.Println("Downloading", asctorrent) + _, err = t.SingleFileDownload("http://idk.i2p/torbrowser/"+asctorrent, filepath.Join(i2psnark, asctorrent), 0) + if err != nil { + return "", "", "", fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + bintorrent := filepath.Join(t.NamePerPlatform(ietf, version) + ".torrent") + fmt.Println("Downloading", bintorrent) + _, err = t.SingleFileDownload("http://idk.i2p/torbrowser/"+bintorrent, filepath.Join(i2psnark, bintorrent), 0) + if err != nil { + return "", "", "", fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + } + for !TorrentDownloaded(ietf, t.GetRuntimePair()) { + log.Println("DownloadUpdaterForLang:", "Waiting for torrent to download") + time.Sleep(time.Second * 10) + } + time.Sleep(time.Second * 10) + } + + sigpath, err := t.SingleFileDownload(sig, t.NamePerPlatform(ietf, version)+".asc", 0) + if err != nil { + return "", "", "", fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + binpath, err := t.SingleFileDownload(binary, t.NamePerPlatform(ietf, version), 0) + if err != nil { + return "", sigpath, "", fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + var sumpath string + if t.OS == "linux" && runtime.GOARCH == "arm64" { + sumpath, err = t.SingleFileDownload("https://sourceforge.net/projects/tor-browser-ports/files/11.0.6/sha256sums-unsigned-build.txt/download", t.NamePerPlatform(ietf, version)+".sha256sums", 0) + if err != nil { + return "", sigpath, sumpath, fmt.Errorf("DownloadUpdaterForLang: %s", err) + } + } + return binpath, sigpath, sumpath, nil +} + +// BrowserDir returns the path to the directory where the browser is installed. +func (t *TBDownloader) BrowserDir() string { + return filepath.Join(t.UnpackPath, "tor-browser_"+t.Lang) +} + +func (t *TBDownloader) I2PBrowserDir() string { + return filepath.Join(t.UnpackPath, "i2p-browser_"+t.Lang) +} + +// UnpackUpdater unpacks the updater to the given path. +// it returns the path or an erorr if one is encountered. +func (t *TBDownloader) UnpackUpdater(binpath string) (string, error) { + if t.NoUnpack { + return binpath, nil + } + t.Log("UnpackUpdater()", fmt.Sprintf("Unpacking %s", binpath)) + if t.OS == "win" { + installPath := t.BrowserDir() + if !FileExists(installPath) { + t.Log("UnpackUpdater()", "Windows updater, running silent NSIS installer") + t.Log("UnpackUpdater()", fmt.Sprintf("Running %s %s %s", binpath, "/S", "/D="+installPath)) + cmd := exec.Command(binpath, "/S", "/D="+installPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("UnpackUpdater: windows exec fail %s", err) + } + if err := cp.Copy(t.BrowserDir(), t.I2PBrowserDir()); err != nil { + return "", fmt.Errorf("UnpackUpdater: copy fail %s", err) + } + } + // copy BrowserDir() to I2PBrowserDir() + + return installPath, nil + } + if t.OS == "osx" { + binpath = "tor-browser/torbrowser-osx64-en-US.dmg" + log.Println("hdiutil", "mount", "\""+binpath+"\"") + //cmd := exec.Command("open", "-W", "-n", "-a", "\""+binpath+"\"") + //cmd := exec.Command("hdiutil", "attach", "\""+binpath+"\"") + consumer := &state.Consumer{ + OnMessage: func(lvl string, msg string) { + log.Printf("[%s] %s", lvl, msg) + }, + } + host := hdiutil.NewHost(consumer) + if !FileExists(t.BrowserDir()) { + if _, err := damage.Mount(host, binpath, t.BrowserDir()); err != nil { + return "", fmt.Errorf("UnpackUpdater: osx open/mount fail %s", err) + } + } + if !FileExists(t.I2PBrowserDir()) { + if _, err := damage.Mount(host, binpath, t.I2PBrowserDir()); err != nil { + return "", fmt.Errorf("UnpackUpdater: osx open/mount fail %s", err) + } + } + //cmd.Stdout = os.Stdout + //cmd.Stderr = os.Stderr + //err := cmd.Run() + //TODO: this might just need to be a hardcoded app path + return t.BrowserDir(), nil + } + if FileExists(t.BrowserDir()) { + if !FileExists(t.I2PBrowserDir()) { + if err := cp.Copy(t.BrowserDir(), t.I2PBrowserDir()); err != nil { + return "", fmt.Errorf("UnpackUpdater: copy fail %s", err) + } + } + return t.BrowserDir(), nil + } + fmt.Fprintf(os.Stderr, "Unpacking %s %s\n", binpath, t.UnpackPath) + os.MkdirAll(t.UnpackPath, 0755) + UNPACK_DIRECTORY, err := os.Open(t.UnpackPath) + if err != nil { + return "", fmt.Errorf("UnpackUpdater: directory error %s", err) + } + defer UNPACK_DIRECTORY.Close() + xzfile, err := os.Open(binpath) + if err != nil { + return "", fmt.Errorf("UnpackUpdater: XZFile error %s", err) + } + defer xzfile.Close() + xzReader, err := xz.NewReader(xzfile) + if err != nil { + return "", fmt.Errorf("UnpackUpdater: XZReader error %s", err) + } + tarReader := tar.NewReader(xzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("UnpackUpdater: Tar looper Error %s", err) + } + if header.Typeflag == tar.TypeDir { + os.MkdirAll(filepath.Join(UNPACK_DIRECTORY.Name(), header.Name), 0755) + continue + } + filename := filepath.Join(UNPACK_DIRECTORY.Name(), header.Name) + file, err := os.Create(filename) + if err != nil { + return "", fmt.Errorf("UnpackUpdater: Tar unpacker error %s", err) + } + defer file.Close() + io.Copy(file, tarReader) + mode := header.FileInfo().Mode() + //remember to chmod the file afterwards + file.Chmod(mode) + if t.Verbose { + fmt.Fprintf(os.Stderr, "Unpacked %s\n", header.Name) + } + } + if !FileExists(t.I2PBrowserDir()) { + if err := cp.Copy(t.BrowserDir(), t.I2PBrowserDir()); err != nil { + return "", fmt.Errorf("UnpackUpdater: copy fail %s", err) + } + } + return t.BrowserDir(), nil +} + +// TorPath returns the path to the Tor executable +func (s *TBDownloader) TorPath() string { + if s.OS == "osx" { + return filepath.Join(s.UnpackPath, "Tor Browser.app", "Contents", "Resources", "TorBrowser", "Tor", "tor") + } + return filepath.Join(s.UnpackPath, "Browser", "TorBrowser", "Tor", "tor") +} + +// CheckSignature checks the signature of the updater. +// it returns an error if one is encountered. If not, it +// runs the updater and returns an error if one is encountered. +func (t *TBDownloader) CheckSignature(binpath, sigpath string) (string, error) { + pk := filepath.Join(t.DownloadPath, "TPO-signing-key.pub") + if t.OS == "linux" && runtime.GOARCH == "arm64" { + pk = filepath.Join(t.DownloadPath, "NOT-TPO-signing-key.pub") + } + var err error + if err = Verify(pk, sigpath, binpath); err == nil { + log.Println("CheckSignature: signature", "verified successfully") + if !t.NoUnpack { + return t.UnpackUpdater(binpath) + } + log.Printf("CheckSignature: %s", "NoUnpack set, skipping unpack") + return t.BrowserDir(), nil + } + return "", fmt.Errorf("CheckSignature: %s", err) +} + +// BoolCheckSignature turns CheckSignature into a bool. +func (t *TBDownloader) BoolCheckSignature(binpath, sigpath string) bool { + _, err := t.CheckSignature(binpath, sigpath) + return err == nil +} + +// TestHTTPDefaultProxy returns true if the I2P proxy is up or blocks until it is. +func TestHTTPDefaultProxy() bool { + return TestHTTPProxy("127.0.0.1", "4444") +} + +// Seconds increments the seconds and displays the number of seconds every 10 seconds +func Seconds(now int) int { + time.Sleep(time.Second) + if now == 3 { + return 0 + } + return now + 1 +} + +// TestHTTPBackupProxy returns true if the I2P backup proxy is up or blocks until it is. +func TestHTTPBackupProxy() bool { + now := 0 + limit := 0 + for { + _, err := net.Listen("tcp", "127.0.0.1:4444") + if err != nil { + log.Println("SAM HTTP proxy is open", err) + return true + } else { + if now == 0 { + log.Println("Waiting for HTTP Proxy", (10 - limit), "remaining attempts") + limit++ + } + now = Seconds(now) + } + if limit == 10 { + break + } + } + return false +} + +// TestHTTPProxy returns true if the proxy at host:port is up or blocks until it is. +func TestHTTPProxy(host, port string) bool { + now := 0 + limit := 0 + for { + proxy := hTTPProxy(host, port) + if proxy { + return true + } else { + if now == 0 { + log.Println("Waiting for HTTP Proxy", (10 - limit), "remaining attempts") + limit++ + } + now = Seconds(now) + } + if limit == 10 { + break + } + } + return false +} + +func hTTPProxy(host, port string) bool { + proxyURL, err := url.Parse("http://" + host + ":" + port) + if err != nil { + log.Panic(err) + } + myClient := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}} + resp, err := myClient.Get("http://proxy.i2p/") + if err == nil { + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err == nil { + return strings.Contains(string(body), "I2P HTTP proxy OK") + } + } + return false +} + +func (t *TBDownloader) MirrorIsI2P() bool { + return MirrorIsI2P(t.Mirror) +} + +func MirrorIsI2P(mirror string) bool { + // check if hostname is an I2P hostname + url, err := url.Parse(mirror) + if err != nil { + return false + } + log.Println("Checking if", url.Hostname(), "is an I2P hostname") + + return strings.Contains(url.Hostname(), ".i2p") +} diff --git a/getffox.go b/getffox.go new file mode 100644 index 0000000..c9dd7f4 --- /dev/null +++ b/getffox.go @@ -0,0 +1,275 @@ +package tbget + +import ( + "archive/tar" + "compress/bzip2" + "embed" + "fmt" + "io" + "log" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// FFOX_UPDATES_URL is the URL to the Firefox updates page +const FFOX_UPDATES_URL string = "https://download.mozilla.org/?product=firefox-latest&os=%s&lang=%s" + +type FFDownloader TBDownloader + +// NewFirefoxDownloader returns a new FFDownloader with the given language, using the FFDownloader's OS/ARCH pair +func NewFirefoxDownloader(lang string, os, arch string, content *embed.FS) *FFDownloader { + OS = os + ARCH = arch + return &FFDownloader{ + Lang: lang, + DownloadPath: DOWNLOAD_FIREFOX_PATH(), + UnpackPath: UNPACK_FIREFOX_PATH(), + OS: os, + ARCH: arch, + Verbose: false, + Profile: content, + Mirror: "https://download.mozilla.org/?product=firefox-latest", + } +} + +// DOWNLOAD_FIREFOX_PATH returns the path to the downloads. +func DOWNLOAD_FIREFOX_PATH() string { + var DOWNLOAD_PATH = filepath.Join(DefaultDir(), "firefox") + return DOWNLOAD_PATH +} + +// UNPACK_FIREFOX_PATH returns the path to the unpacked files. +func UNPACK_FIREFOX_PATH() string { + var UNPACK_FIREFOX_PATH = filepath.Join(DefaultDir(), "unpack-firefox") + return UNPACK_FIREFOX_PATH +} + +func (t FFDownloader) GetRuntimePair() string { + tbd := TBDownloader(t) + return tbd.GetRuntimePair() +} + +// GetLatestFirefoxVersionURL returns the URL to the latest Firefox version for the given os and lang +func (t *FFDownloader) GetLatestFirefoxVersionURL(os, lang string) string { + return fmt.Sprintf(FFOX_UPDATES_URL, t.GetRuntimePair(), lang) +} + +// GetLatestFirefoxVersionLinuxSigURL returns the URL to the latest Firefox version detatched signature for the given os and lang +func (t *FFDownloader) GetLatestFirefoxVersionLinuxSigURL(os, lang string) string { + return t.GetLatestFirefoxVersionURL(os, lang) + ".asc" +} + +// GetFirefoxUpdater gets the updater URL for the t.Lang. It returns +// the URL, a detatched sig if available for the platform, or an error +func (t *FFDownloader) GetFirefoxUpdater() (string, string, error) { + return t.GetLatestFirefoxVersionURL(t.OS, t.Lang), t.GetLatestFirefoxVersionLinuxSigURL(t.OS, t.Lang), nil +} + +// GetFirefoxUpdaterForLang gets the updater URL for the given language, overriding +// the t.Lang. It returns the URL, a detatched sig if available for the platform, or an error +func (t *FFDownloader) GetFirefoxUpdaterForLang(ietf string) (string, string, error) { + return t.GetLatestFirefoxVersionURL(t.OS, ietf), t.GetLatestFirefoxVersionLinuxSigURL(t.OS, ietf), nil +} + +// SendFirefoxVersionHEADRequest sends a HEAD request to the Firefox version URL +func (t *FFDownloader) SendFirefoxVersionHEADRequest() (string, error) { + url := t.GetLatestFirefoxVersionURL(t.OS, t.Lang) + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return "", fmt.Errorf("t.SendFirefoxVersionHEADRequest: %s", err) + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:68.0) Gecko/20100101 Firefox/68.0") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("t.SendFirefoxVersionHEADRequest: %s", err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return "", fmt.Errorf("t.SendFirefoxVersionHEADRequest: %s", resp.Status) + } + return resp.Header.Get("Location"), nil +} + +// ExtractFirefoxVersion extracts the Firefox version from the updater URL +func (t *FFDownloader) ExtractFirefoxVersion() (string, error) { + url, err := t.SendFirefoxVersionHEADRequest() + if err != nil { + return "", fmt.Errorf("t.ExtractFirefoxVersion: %s", err) + } + // get the last element of the URL + url = strings.Split(url, "/")[len(strings.Split(url, "/"))-1] + // remove all file extensions + url = strings.Replace(url, ".tar.xz", "", -1) + url = strings.Replace(url, ".tar.bz2", "", -1) + url = strings.Replace(url, ".tar.gz", "", -1) + url = strings.Replace(url, ".zip", "", -1) + url = strings.Replace(url, ".exe", "", -1) + url = strings.Replace(url, ".msi", "", -1) + url = strings.Replace(url, ".dmg", "", -1) + return url, nil +} + +// NamePerPlatformFirefox returns the name of the Firefox package per platform. +func (t *FFDownloader) NamePerPlatformFirefox(ietf string) string { + extension := "tar.bz2" + windowsonly := "" + switch t.OS { + case "osx": + extension = "dmg" + case "win": + windowsonly = "-setup" + extension = "exe" + } + return fmt.Sprintf("firefox%s-%s-%s.%s", windowsonly, t.GetRuntimePair(), ietf, extension) +} + +// FirefoxBrowserDir returns the path to the directory where the Firefox browser is installed. +func (t *FFDownloader) FirefoxBrowserDir() string { + return filepath.Join(t.UnpackPath, "firefox_"+t.Lang) +} + +func (t *FFDownloader) Log(function, message string) { + if t.Verbose { + log.Println(fmt.Sprintf("%s: %s", function, message)) + } +} + +// UnpackFirefox unpacks the Firefox package to the t.FirefoxBrowserDir() +func (t *FFDownloader) UnpackFirefox(binpath string) (string, error) { + t.Log("UnpackFirefox()", fmt.Sprintf("Unpacking %s", binpath)) + if t.OS == "win" { + installPath := t.FirefoxBrowserDir() + if !FileExists(installPath) { + t.Log("UnpackFirefox()", "Windows updater, running silent NSIS installer") + t.Log("UnpackFirefox()", fmt.Sprintf("Running %s %s %s", binpath, "/S", "/D="+installPath)) + cmd := exec.Command(binpath, "/S", "/D="+installPath) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("UnpackFirefox: windows exec fail %s", err) + } + } + return installPath, nil + } + if t.OS == "osx" { + cmd := exec.Command("open", "-W", "-n", "-a", "\""+t.UnpackPath+"\"", "\""+binpath+"\"") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return "", fmt.Errorf("UnpackFirefox: osx open/mount fail %s", err) + } + //TODO: this might just need to be a hardcoded app path + return t.UnpackPath, nil + } + if FileExists(t.FirefoxBrowserDir()) { + return t.FirefoxBrowserDir(), nil + } + fmt.Printf("Unpacking %s %s\n", binpath, t.UnpackPath) + os.MkdirAll(t.UnpackPath, 0755) + UNPACK_DIRECTORY, err := os.Open(t.UnpackPath) + if err != nil { + return "", fmt.Errorf("UnpackFirefox: directory error %s", err) + } + defer UNPACK_DIRECTORY.Close() + bzfile, err := os.Open(binpath) + if err != nil { + return "", fmt.Errorf("UnpackFirefox: BZFile error %s", err) + } + defer bzfile.Close() + bzReader := bzip2.NewReader(bzfile) + tarReader := tar.NewReader(bzReader) + for { + header, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("UnpackFirefox: Tar looper Error %s", err) + } + if header.Typeflag == tar.TypeDir { + os.MkdirAll(filepath.Join(UNPACK_DIRECTORY.Name(), header.Name), 0755) + continue + } + filename := filepath.Join(UNPACK_DIRECTORY.Name(), header.Name) + file, err := os.Create(filename) + if err != nil { + //return "", + fmt.Printf("UnpackFirefox: Tar unpacker error %s", err) + } + defer file.Close() + io.Copy(file, tarReader) + mode := header.FileInfo().Mode() + //remember to chmod the file afterwards + file.Chmod(mode) + if t.Verbose { + fmt.Printf("Unpacked %s\n", header.Name) + } + } + return t.FirefoxBrowserDir(), nil +} + +// DownloadFirefoxUpdater downloads the updater for the t.Lang. It returns +// the path to the downloaded updater and the downloaded detatched signature, +// or an error if one is encountered. +func (t *FFDownloader) DownloadFirefoxUpdater() (string, string, error) { + return t.DownloadFirefoxUpdaterForLang(t.Lang) +} + +func (t FFDownloader) SingleFileDownload(dl, name string, rangebottom int64) (string, error) { + tbd := TBDownloader(t) + return tbd.SingleFileDownload(dl, name, rangebottom) +} + +// DownloadFirefoxUpdaterForLang downloads the updater for the given language, overriding +// t.Lang. It returns the path to the downloaded updater and the downloaded +// detatched signature, or an error if one is encountered. +func (t *FFDownloader) DownloadFirefoxUpdaterForLang(ietf string) (string, string, error) { + binary, sig, err := t.GetFirefoxUpdaterForLang(ietf) + if err != nil { + return "", "", fmt.Errorf("DownloadUpdater: %s", err) + } + sigpath := "" + if t.OS == "linux" { + sigpath, err = t.SingleFileDownload(sig, t.NamePerPlatformFirefox(ietf)+".asc", 0) + if err != nil { + return "", "", fmt.Errorf("DownloadUpdater: %s", err) + } + } + binpath, err := t.SingleFileDownload(binary, t.NamePerPlatformFirefox(ietf), 0) + if err != nil { + return "", "", fmt.Errorf("DownloadUpdater: %s", err) + } + return binpath, sigpath, nil +} + +// CheckFirefoxSignature checks the signature of the updater. +// it returns an error if one is encountered. If not, it +// runs the updater and returns an error if one is encountered. +func (t *FFDownloader) CheckFirefoxSignature(binpath, sigpath string) (string, error) { + if t.OS == "linux" { + var err error + pk := filepath.Join(t.DownloadPath, "TPO-signing-key.pub") + if err = Verify(pk, sigpath, binpath); err == nil { + t.Log("CheckFirefoxSignature: signature", "verified successfully") + return t.UnpackFirefox(binpath) + } + return "", fmt.Errorf("CheckSignature: %s", err) + } + return "", nil +} + +// BoolCheckFirefoxSignature turns CheckFirefoxSignature into a bool. +func (t *FFDownloader) BoolCheckFirefoxSignature(binpath, sigpath string) bool { + _, err := t.CheckFirefoxSignature(binpath, sigpath) + return err == nil +} + +func (t FFDownloader) MakeTBDirectory() { + tbd := TBDownloader(t) + tbd.MakeTBDirectory() +} diff --git a/gpg.go b/gpg.go new file mode 100644 index 0000000..cd13d79 --- /dev/null +++ b/gpg.go @@ -0,0 +1,40 @@ +package tbget + +import ( + "fmt" + "log" + "os" + + "github.com/ProtonMail/go-crypto/openpgp" +) + +func Verify(keyrings, detached, target string) error { + keyRingReader, err := os.Open(keyrings) + if err != nil { + return fmt.Errorf("Verify: failed to open keyrings: %s\n\t%s", err, keyrings) + } + + signature, err := os.Open(detached) + if err != nil { + return fmt.Errorf("Verify: failed to open detached signature: %s\n\t%s", err, detached) + } + + verification_target, err := os.Open(target) + if err != nil { + return fmt.Errorf("Verify: failed to open verification target: %s\n\t%s", err, target) + } + + entities, err := openpgp.ReadArmoredKeyRing(keyRingReader) + if err != nil { + return fmt.Errorf("Verify: failed to read keyrings: %s\n\t%s", err, keyrings) + } + log.Printf("Verify: %s", fmt.Sprintf("Read %d keyrings", len(entities))) + log.Printf("Verifying: %s against %s\n", target, detached) + log.Printf("Verify: using keyring %s\n", keyrings) + _, err = openpgp.CheckArmoredDetachedSignature(entities, verification_target, signature, nil) + if err != nil { + return fmt.Errorf("Verify: failed to verify signature: %s\n\t%s\n\t%s\n\t%s", err, keyrings, detached, target) + } + + return nil +} diff --git a/torrent.go b/torrent.go new file mode 100644 index 0000000..c70ccae --- /dev/null +++ b/torrent.go @@ -0,0 +1,325 @@ +package tbget + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/cloudfoundry/jibber_jabber" + "github.com/eyedeekay/i2pkeys" + cp "github.com/otiai10/copy" + "github.com/xgfone/bt/bencode" + "github.com/xgfone/bt/metainfo" +) + +func (t *TBDownloader) DownloadedFilesList() ([]string, error) { + files, err := ioutil.ReadDir(t.DownloadPath) + if err != nil { + return nil, fmt.Errorf("DownloadedFilesList: %s", err) + } + var list []string + for _, f := range files { + list = append(list, f.Name()) + } + return list, nil + +} + +func (t *TBDownloader) GenerateMissingTorrents() error { + files, err := t.DownloadedFilesList() + if err != nil { + return err + } + for _, f := range files { + fp := filepath.Join(t.DownloadPath, f+".torrent") + af := filepath.Join(t.DownloadPath, f) + if !strings.HasSuffix(af, ".torrent") { + //os.Remove(fp) + if !FileExists(fp) { + log.Println("Generating torrent for", fp) + meta, err := t.GenerateTorrent(af, nil) + if err != nil { + //return err + log.Println("GenerateMissingTorrents:", err) + continue + } + file, err := os.Create(fp) + if err != nil { + return err + } + meta.Write(file) + file.Close() + } + snark, err := FindSnarkDirectory() + if err != nil { + return err + } + sf := filepath.Join(snark, f) + sfp := filepath.Join(snark, f+".torrent") + if !FileExists(sf) { + log.Println("Copying", af, "to", sf) + cp.Copy(af, sf) + } + if !FileExists(sfp) { + log.Println("Copying", fp, "to", sfp) + cp.Copy(fp, sfp) + } + } + } + return nil +} + +func (t *TBDownloader) GenerateTorrent(file string, announces []string) (*metainfo.MetaInfo, error) { + + //info, err := metainfo.NewInfoFromFilePath(file, 5120) + info, err := metainfo.NewInfoFromFilePath(file, 10240) + if err != nil { + return nil, fmt.Errorf("GenerateTorrent: %s", err) + } + info.Name = filepath.Base(file) + + var mi metainfo.MetaInfo + mi.InfoBytes, err = bencode.EncodeBytes(info) + if err != nil { + return nil, fmt.Errorf("GenerateTorrent: %s", err) + } + + switch len(announces) { + case 0: + mi.Announce = "http://mb5ir7klpc2tj6ha3xhmrs3mseqvanauciuoiamx2mmzujvg67uq.b32.i2p/a" + case 1: + mi.Announce = announces[0] + default: + mi.AnnounceList = metainfo.AnnounceList{announces} + } + url, err := url.Parse("http://idk.i2p/torbrowser/" + filepath.Base(file)) + if err != nil { + return nil, fmt.Errorf("GenerateTorrent: %s", err) + } + mi.URLList = []string{url.String()} + if t.listener != nil { + url, err := url.Parse("http://" + t.listener.Addr().(i2pkeys.I2PAddr).Base32() + "/" + filepath.Base(file)) + if err != nil { + return nil, fmt.Errorf("GenerateTorrent: %s", err) + } + if t.Mirror != "" { + mi.URLList = append(mi.URLList, url.String()) + } + } + clearurl, err := url.Parse("https://eyedeekay.github.io/torbrowser/" + filepath.Base(file)) + if err != nil { + return nil, fmt.Errorf("GenerateTorrent: %s", err) + } + mi.URLList = append(mi.URLList, clearurl.String()) + return &mi, nil +} + +func FindSnarkDirectory() (string, error) { + // Snark could be at: + // or: $I2P_CONFIG/i2psnark/ + // or: $I2P/i2psnark/ + // or: $HOME/.i2p/i2psnark/ + // or: /var/lib/i2p/i2p-config/i2psnark/ + // or: %LOCALAPPDATA\i2p\i2psnark\ + // or: %APPDATA\i2p\i2psnark\ + + SNARK_CONFIG := os.Getenv("SNARK_CONFIG") + if SNARK_CONFIG != "" { + checkfori2pcustom := filepath.Join(SNARK_CONFIG) + if FileExists(checkfori2pcustom) { + //log.Println("Found snark directory at $SNARK_CONFIG", checkfori2pcustom) + return checkfori2pcustom, nil + } + } + + I2P_CONFIG := os.Getenv("I2P_CONFIG") + if I2P_CONFIG != "" { + checkfori2pcustom := filepath.Join(I2P_CONFIG, "i2psnark") + if FileExists(checkfori2pcustom) { + //log.Println("Found snark directory at $I2P_CONFIG", checkfori2pcustom) + return checkfori2pcustom, nil + } + } + + I2P := os.Getenv("I2P") + if I2P != "" { + checkfori2p := filepath.Join(I2P, "i2psnark") + if FileExists(checkfori2p) { + //log.Println("Found snark directory at $I2P", checkfori2p) + return checkfori2p, nil + } + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + // Start by getting the home directory + switch runtime.GOOS { + case "windows": + checkfori2plocal := filepath.Join(home, "AppData", "Local", "i2p", "i2psnark") + if FileExists(checkfori2plocal) { + //log.Println("Found snark directory at %APPDATA%\\i2p\\i2psnark", "%APPDATA%\\i2p\\i2psnark") + return checkfori2plocal, nil + } + checkfori2proaming := filepath.Join(home, "AppData", "Roaming", "i2p", "i2psnark") + if FileExists(checkfori2proaming) { + //log.Println("Found snark directory at %APPDATA%\\i2p\\i2psnark", "%APPDATA%\\i2p\\i2psnark") + return checkfori2proaming, nil + } + case "linux": + checkfori2phome := filepath.Join(home, ".i2p", "i2psnark") + if FileExists(checkfori2phome) { + //log.Println("Found snark directory at $HOME/.i2p/i2psnark", "$HOME/.i2p/i2psnark") + return checkfori2phome, nil + } + checkfori2pservice := filepath.Join("/var/lib/i2p/i2p-config", "i2psnark") + if FileExists(checkfori2pservice) { + //log.Println("Found snark directory at /var/lib/i2p/i2p-config/i2psnark", checkfori2pservice) + return checkfori2pservice, nil + } + case "darwin": + return "", fmt.Errorf("FindSnarkDirectory: Automatic torrent generation is not supported on MacOS, for now copy the files manually") + } + return "", fmt.Errorf("FindSnarkDirectory: Unable to find snark directory") +} + +func TorrentReady() bool { + if _, err := FindSnarkDirectory(); err != nil { + return false + } + return true +} + +func TorrentPath() (string, string) { + extension := "tar.xz" + windowsonly := "" + switch runtime.GOOS { + case "darwin": + extension = "dmg" + case "windows": + windowsonly = "-installer" + extension = "exe" + } + //version, err := t.Get + return fmt.Sprintf("tor-browser%s", windowsonly), extension +} + +func GetTorBrowserVersionFromUpdateURL() (string, error) { + // download the json file from TOR_UPDATES_URL + // parse the json file to get the latest version + // return the latest version + err := SetupProxy(TOR_UPDATES_URL, "") + if err != nil { + return "", err + } + resp, err := http.Get(TOR_UPDATES_URL) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + var updates map[string]interface{} + err = json.Unmarshal(body, &updates) + if err != nil { + return "", err + } + latest := updates["downloads"].(map[string]interface{})["linux64"].(map[string]interface{})["en-US"].(map[string]interface{}) + + for key, value := range latest { + if key == "binary" { + log.Printf("%s: %s\n", key, value) + url, err := url.Parse(value.(string)) + if err != nil { + return "", err + } + spl := strings.Split(url.Path, "/") + return spl[len(spl)-2], nil + } + } + + return "Unknown", nil +} + +func TorrentDownloaded(ietf, rtpair string) bool { + if ietf == "" { + var err error + ietf, err = jibber_jabber.DetectIETF() + if err != nil { + panic(err) + } + } + version, err := GetTorBrowserVersionFromUpdateURL() + if err != nil { + return false + } + log.Println("Tor Browser Version", version, ietf) + extension := "exe" + if strings.Contains(rtpair, "linux") { + extension = "tar.xz" + } + if strings.Contains(rtpair, "osx") { + extension = "dmg" + } + cmpsize, err := FetchContentLength(fmt.Sprintf("https://dist.torproject.org/torbrowser/%s/tor-browser-%s-%s_%s.%s", version, rtpair, version, ietf, extension), fmt.Sprintf("tor-browser-%s-%s_%s.%s", rtpair, version, ietf, extension)) + if err != nil { + //panic(err) + return TorrentDownloaded(ietf, rtpair) + } + found := false + if dir, err := FindSnarkDirectory(); err == nil { + err := filepath.Walk(dir, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + prefix, suffix := TorrentPath() + path = filepath.Base(path) + if strings.HasPrefix(path, prefix) { + if strings.Contains(path, "_"+ietf) { + if strings.Contains(path, version) { + if strings.Contains(path, rtpair) { + if strings.HasSuffix(path, suffix) { + if info.Size() == int64(cmpsize) { + sizeString := fmt.Sprintf("%d", info.Size()) + cmpString := fmt.Sprintf("%d", cmpsize) + fmt.Fprintf(os.Stderr, "TorrentDownloaded: Torrent Download complete: %v %v %v %v %v", path, info.Size(), int64(cmpsize), len(sizeString), len(cmpString)) + found = true + return nil + } else { + fmt.Fprintf(os.Stderr, "TorrentDownloaded: Torrent Download incomplete: %v %v %v", path, info.Size(), int64(cmpsize)) + return fmt.Errorf("TorrentDownloaded: Torrent Download found but size is too small: %s", path) + } + } + } + } + } + } + return nil + }) + if found { + return err == nil + } + return false + } + return false +} + +func Torrent(ietf, rtpair string) bool { + if !TorrentReady() { + return false + } + if !TorrentDownloaded(ietf, rtpair) { + return false + } + return true +}