Skip to content
Snippets Groups Projects
Select Git revision
  • c89254fd0f7f4517163e095f144bfe154f47758a
  • wip-bootstrap default
  • dualcore
  • ch3/leds
  • ch3/time
  • master
6 results

modubinascii.c

Blame
  • main.go 6.78 KiB
    package main
    
    import (
    	"archive/tar"
    	"bytes"
    	"compress/bzip2"
    	"context"
    	"encoding/json"
    	"flag"
    	"fmt"
    	"io"
    	"log"
    	"net/http"
    	"net/url"
    	"regexp"
    	"strings"
    	"sync"
    	"time"
    )
    
    var (
    	flagBaseURL       string
    	flagListen        string
    	flagGitlabHost    string
    	flagGitlabProject string
    )
    
    type GLAssetLink struct {
    	ID      int64  `json:"id"`
    	Name    string `json:"name"`
    	TagName string `json:"tag_name"`
    	URL     string `json:"url"`
    }
    
    type GLRelease struct {
    	Name       string    `json:"name"`
    	TagName    string    `json:"tag_name"`
    	ReleasedAt time.Time `json:"released_at"`
    	Assets     struct {
    		Links []GLAssetLink `json:"links"`
    	} `json:"assets"`
    }
    
    func getReleases(ctx context.Context) ([]GLRelease, error) {
    	path := fmt.Sprintf("https://%s/api/v4/projects/%s/releases?order_by=created_at", flagGitlabHost, url.PathEscape(flagGitlabProject))
    	req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
    	if err != nil {
    		return nil, fmt.Errorf("when building request: %w", err)
    	}
    	res, err := http.DefaultTransport.RoundTrip(req)
    	if err != nil {
    		return nil, fmt.Errorf("when performing request: %w", err)
    	}
    	defer res.Body.Close()
    
    	if res.StatusCode != 200 {
    		return nil, fmt.Errorf("invalid response code %d", res.StatusCode)
    	}
    
    	var releases []GLRelease
    	j := json.NewDecoder(res.Body)
    	err = j.Decode(&releases)
    	if err != nil {
    		return nil, fmt.Errorf("when performing request: %w", err)
    	}
    	return releases, nil
    }
    
    type req struct {
    	getReleases chan []GLRelease
    }
    
    type server struct {
    	reqC chan *req
    
    	cacheMu sync.RWMutex
    	cache   map[string]map[string][]byte
    }
    
    func (s *server) run(ctx context.Context) {
    	releases, err := getReleases(ctx)
    	if err != nil {
    		log.Fatalf("Initial release fetch failed: %v", err)
    	}
    
    	t := time.NewTicker(60 * time.Second)
    	for {
    		select {
    		case r := <-s.reqC:
    			if r.getReleases != nil {
    				r.getReleases <- releases
    			}
    		case <-t.C:
    			releases, err = getReleases(ctx)
    			if err != nil {
    				log.Printf("Failed to fetch releases: %v", err)
    			}
    		}
    	}
    }
    
    func (s *server) getReleases(ctx context.Context) ([]GLRelease, error) {
    	sresC := make(chan []GLRelease)
    	select {
    	case s.reqC <- &req{getReleases: sresC}:
    		releases := <-sresC
    		return releases, nil
    	case <-ctx.Done():
    		return nil, ctx.Err()
    	}
    }
    
    func (s *server) handleReleases(w http.ResponseWriter, r *http.Request) {
    	ctx := r.Context()
    	releases, err := s.getReleases(ctx)
    	if err != nil {
    		return
    	}
    
    	type partition struct {
    		Name   string `json:"name"`
    		URL    string `json:"url"`
    		Offset string `json:"offset"`
    	}
    
    	type release struct {
    		Name       string      `json:"name"`
    		Partitions []partition `json:"partitions"`
    	}
    
    	var resp []release
    	for _, rel := range releases {
    		var partitions []partition
    		offsets := map[string]int64{
    			"bootloader":      0,
    			"partition-table": 0x8000,
    			"recovery":        0x10000,
    			"flow3r":          0x90000,
    		}
    		for _, pname := range []string{"bootloader", "partition-table", "recovery", "flow3r"} {
    			partitions = append(partitions, partition{
    				Name:   pname,
    				URL:    fmt.Sprintf("%srelease/%s/%s.bin", flagBaseURL, rel.TagName, pname),
    				Offset: fmt.Sprintf("0x%x", offsets[pname]),
    			})
    		}
    		resp = append(resp, release{
    			Name:       rel.Name,
    			Partitions: partitions,
    		})
    	}
    	w.Header().Add("Content-Type", "application/json")
    	j := json.NewEncoder(w)
    	j.Encode(resp)
    }
    
    var (
    	reMirrorURL = regexp.MustCompile("^/api/release/([^/]+)/([^/]+.bin)$")
    )
    
    func (s *server) cacheTarball(rel *GLRelease, data io.Reader) error {
    	bz2 := bzip2.NewReader(data)
    	t := tar.NewReader(bz2)
    
    	log.Printf("Extracting %q...", rel.TagName)
    	cacheEntry := make(map[string][]byte)
    	for {
    		hdr, err := t.Next()
    		if err == io.EOF {
    			break
    		}
    		if err != nil {
    			return fmt.Errorf("when reading header: %v", err)
    		}
    		parts := strings.Split(hdr.Name, "/")
    		last := parts[len(parts)-1]
    		log.Printf("%q has %q", rel.TagName, last)
    		buf := bytes.NewBuffer(nil)
    		if _, err := io.Copy(buf, t); err != nil {
    			return fmt.Errorf("file %s: %v", hdr.Name, err)
    		}
    		cacheEntry[last] = buf.Bytes()
    	}
    	s.cacheMu.Lock()
    	s.cache[rel.TagName] = cacheEntry
    	s.cacheMu.Unlock()
    	return nil
    }
    
    func (s *server) serveMirroredFile(w http.ResponseWriter, r *http.Request, rel *GLRelease, artifact string) {
    	ctx := r.Context()
    
    	s.cacheMu.RLock()
    	if s.cache[rel.TagName] != nil && s.cache[rel.TagName][artifact] != nil {
    		b := s.cache[rel.TagName][artifact]
    		s.cacheMu.RUnlock()
    		w.Header().Add("Content-Type", "application/octet-stream")
    		w.Write(b)
    		return
    	}
    	if s.cache[rel.TagName] != nil {
    		log.Printf("Failed: no %q in %q", artifact, rel.TagName)
    		http.NotFound(w, r)
    		return
    	}
    	s.cacheMu.RUnlock()
    
    	log.Printf("Fetching %q (for %q)...", rel.TagName, artifact)
    
    	if len(rel.Assets.Links) != 1 {
    		log.Printf("Tag %s has %d assets", rel.TagName, len(rel.Assets.Links))
    		http.NotFound(w, r)
    		return
    	}
    	link := rel.Assets.Links[0]
    	req, err := http.NewRequestWithContext(ctx, "GET", link.URL, nil)
    	if err != nil {
    		w.WriteHeader(500)
    		fmt.Fprintf(w, "could not download")
    		return
    	}
    	res, err := http.DefaultTransport.RoundTrip(req)
    	if err != nil {
    		w.WriteHeader(500)
    		fmt.Fprintf(w, "could not download")
    		return
    	}
    	defer res.Body.Close()
    
    	if err := s.cacheTarball(rel, res.Body); err != nil {
    		w.WriteHeader(500)
    		fmt.Fprintf(w, "could not cache")
    		return
    	}
    
    	s.cacheMu.RLock()
    	if s.cache[rel.TagName] != nil && s.cache[rel.TagName][artifact] != nil {
    		b := s.cache[rel.TagName][artifact]
    		s.cacheMu.RUnlock()
    		w.Header().Add("Content-Type", "application/octet-stream")
    		w.Write(b)
    		return
    	}
    	s.cacheMu.RUnlock()
    	http.NotFound(w, r)
    }
    
    func (s *server) handleReleaseMirror(w http.ResponseWriter, r *http.Request) {
    	ctx := r.Context()
    
    	matches := reMirrorURL.FindStringSubmatch(r.URL.Path)
    	if matches == nil {
    		http.NotFound(w, r)
    		return
    	}
    	tag := matches[1]
    	artifact := matches[2]
    
    	releases, err := s.getReleases(ctx)
    	if err != nil {
    		return
    	}
    
    	for _, rel := range releases {
    		if rel.TagName == tag {
    			s.serveMirroredFile(w, r, &rel, artifact)
    			return
    		}
    	}
    }
    
    func main() {
    	flag.StringVar(&flagBaseURL, "base_url", "https://flow3r.garden/api/", "Base address at which this instance runs (used for calculating proxied data URLs)")
    	flag.StringVar(&flagListen, "listen", ":8080", "Address on which to listen")
    	flag.StringVar(&flagGitlabHost, "gitlab_host", "git.flow3r.garden", "GitLab instance host")
    	flag.StringVar(&flagGitlabProject, "gitlab_project", "flow3r/flow3r-firmware", "Name of the organization/project on GitLab")
    	flag.Parse()
    
    	ctx := context.Background()
    	s := server{
    		reqC:  make(chan *req),
    		cache: make(map[string]map[string][]byte),
    	}
    	go s.run(ctx)
    
    	http.HandleFunc("/api/releases.json", s.handleReleases)
    	http.HandleFunc("/api/release/", s.handleReleaseMirror)
    	log.Printf("Listening on %s...", flagListen)
    	http.ListenAndServe(flagListen, nil)
    }