games/factorio: add modproxy

This adds a mod proxy system, called, well, modproxy.

It sits between Factorio server instances and the Factorio mod portal,
allowing for arbitrary mod download without needing the servers to know
Factorio credentials.

Change-Id: I7bc405a25b6f9559cae1f23295249f186761f212
diff --git a/games/factorio/modproxy/modportal/modportal.go b/games/factorio/modproxy/modportal/modportal.go
new file mode 100644
index 0000000..5e507fd
--- /dev/null
+++ b/games/factorio/modproxy/modportal/modportal.go
@@ -0,0 +1,98 @@
+package modportal
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+)
+
+type Mod struct {
+	Name     string    `json:"name"`
+	Releases []Release `json:"releases"`
+}
+
+type Release struct {
+	DownloadURL string   `json:"download_url"`
+	FileName    string   `json:"file_name"`
+	Info        InfoJSON `json:"info_json"`
+	ReleasedAt  string   `json:"released_at"`
+	SHA1        string   `json:"sha1"`
+	Version     string   `json:"version"`
+}
+
+type InfoJSON struct {
+	Dependencies    []string `json:"dependencies"`
+	FactorioVersion string   `json:"factorio_json"`
+}
+
+func GetMod(ctx context.Context, name string) (*Mod, error) {
+	url := fmt.Sprintf("https://mods.factorio.com/api/mods/%s", url.PathEscape(name))
+
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "NewRequest(%q): %v", url, err)
+	}
+
+	req = req.WithContext(ctx)
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, status.Errorf(codes.Unavailable, "Do(req{%q}): %v", url, err)
+	}
+	defer res.Body.Close()
+
+	if res.StatusCode != 200 {
+		return nil, status.Errorf(codes.Unavailable, "mod portal responded with code: %d", res.StatusCode)
+	}
+
+	mod := &Mod{}
+	err = json.NewDecoder(res.Body).Decode(mod)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "could not decode mod portal JSON: %v", err)
+	}
+
+	return mod, nil
+}
+
+func (m *Mod) ReleaseBySHA1(sha1 string) *Release {
+	for _, r := range m.Releases {
+		if r.SHA1 == sha1 {
+			return &r
+		}
+	}
+	return nil
+}
+
+func (m *Mod) ReleaseByVersion(version string) *Release {
+	for _, r := range m.Releases {
+		if r.Version == version {
+			return &r
+		}
+	}
+	return nil
+}
+
+func (r *Release) Download(ctx context.Context, username, token string) (io.ReadCloser, error) {
+	url := fmt.Sprintf("https://mods.factorio.com/%s?username=%s&token=%s", r.DownloadURL, username, token)
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, status.Errorf(codes.Internal, "NewRequest(%q): %v", url, err)
+	}
+
+	req = req.WithContext(ctx)
+	res, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, status.Errorf(codes.Unavailable, "Do(req{%q}): %v", url, err)
+	}
+	if res.StatusCode != 200 {
+		res.Body.Close()
+		return nil, status.Errorf(codes.Unavailable, "mod portal responded with code: %d", res.StatusCode)
+	}
+
+	return res.Body, err
+}