Skip to content

Commit 32b3cc0

Browse files
committed
Initial commit
0 parents  commit 32b3cc0

12 files changed

+471
-0
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.aptcache
2+
apt-proxy

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.aptcache
2+
apt-proxy

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM ubuntu:14.10
2+
3+
RUN apt-get update
4+
RUN apt-get install -y golang git
5+
6+
ENV GOPATH /app
7+
ADD . /app/src/github.com/lox/apt-proxy
8+
9+
WORKDIR /app/src/github.com/lox/apt-proxy
10+
RUN go get
11+
12+
EXPOSE 8080
13+
CMD ["go", "run", "/app/src/github.com/lox/apt-proxy/apt-proxy.go"]

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Apt Proxy
2+
3+
A caching proxy specifically for apt package caching, also rewrites to the fastest local mirror.
4+
5+
Built because [apt-cacher-ng](https://www.unix-ag.uni-kl.de/~bloch/acng/) is unreliable.
6+
7+
## Running via Go
8+
9+
```
10+
go install github.com/lox/apt-proxy
11+
$GOBIN/apt-proxy
12+
```

apt-proxy.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"log"
6+
"net/http"
7+
"time"
8+
9+
"github.com/lox/apt-proxy/proxy"
10+
"github.com/lox/httpcache"
11+
)
12+
13+
const (
14+
defaultListen = "0.0.0.0:3142"
15+
defaultDir = "./.aptcache"
16+
)
17+
18+
var cachePatterns = proxy.CachePatternSlice{
19+
proxy.NewCachePattern(`deb$`, time.Hour*24*7),
20+
proxy.NewCachePattern(`udeb$`, time.Hour*24*7),
21+
proxy.NewCachePattern(`DiffIndex$`, time.Hour),
22+
proxy.NewCachePattern(`PackagesIndex$`, time.Hour),
23+
proxy.NewCachePattern(`Packages\.(bz2|gz|lzma)$`, time.Hour),
24+
proxy.NewCachePattern(`SourcesIndex$`, time.Hour),
25+
proxy.NewCachePattern(`Sources\.(bz2|gz|lzma)$`, time.Hour),
26+
proxy.NewCachePattern(`Release(\.gpg)?$`, time.Hour),
27+
proxy.NewCachePattern(`Translation-(en|fr)\.(gz|bz2|bzip2|lzma)$`, time.Hour),
28+
proxy.NewCachePattern(`Sources\.lzma$`, time.Hour),
29+
}
30+
31+
var (
32+
version string
33+
listen string
34+
dir string
35+
)
36+
37+
func init() {
38+
flag.StringVar(&listen, "listen", defaultListen, "the host and port to bind to")
39+
flag.StringVar(&dir, "cachedir", defaultDir, "the dir to store cache data in")
40+
flag.Parse()
41+
}
42+
43+
func main() {
44+
log.Printf("running apt-proxy %s", version)
45+
46+
cache, err := httpcache.NewDiskCache(dir)
47+
if err != nil {
48+
log.Fatal(err)
49+
}
50+
51+
ap := proxy.NewAptProxy()
52+
ap.CachePatterns = cachePatterns
53+
54+
log.Printf("proxy listening on https://%s", listen)
55+
log.Fatal(http.ListenAndServe(listen, &httpcache.Logger{
56+
Handler: httpcache.NewHandler(cache, ap.Handler()),
57+
}))
58+
}

http_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package main_test
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"net/http/httptest"
8+
"net/url"
9+
"testing"
10+
"time"
11+
12+
"github.com/lox/apt-proxy/proxy"
13+
)
14+
15+
type testFixture struct {
16+
proxy, backend *httptest.Server
17+
}
18+
19+
func newTestFixture(handler http.HandlerFunc, ap *proxy.AptProxy) *testFixture {
20+
backend := httptest.NewServer(http.HandlerFunc(handler))
21+
backendURL, err := url.Parse(backend.URL)
22+
if err != nil {
23+
panic(err)
24+
}
25+
26+
ap.Rewriters = append(ap.Rewriters, proxy.RewriterFunc(func(req *http.Request) {
27+
req.URL.Host = backendURL.Host
28+
}))
29+
30+
return &testFixture{httptest.NewServer(ap.Handler()), backend}
31+
}
32+
33+
// client returns an http client configured to use the provided proxy
34+
func (f *testFixture) client() *http.Client {
35+
proxyURL, err := url.Parse(f.proxy.URL)
36+
if err != nil {
37+
panic(err)
38+
}
39+
40+
return &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
41+
}
42+
43+
func (f *testFixture) close() {
44+
f.proxy.Close()
45+
f.backend.Close()
46+
}
47+
48+
func assertHeader(t *testing.T, r *http.Response, header string, expected string) {
49+
if r.Header.Get(header) != expected {
50+
t.Fatalf("Expected header %s=%s, but got '%s'",
51+
header, expected, r.Header.Get(header))
52+
}
53+
}
54+
55+
func TestProxyAddsCacheControlHeader(t *testing.T) {
56+
handler := func(w http.ResponseWriter, r *http.Request) {
57+
w.Write([]byte("Llamas rock"))
58+
}
59+
ap := proxy.NewAptProxy()
60+
ap.CachePatterns = append(ap.CachePatterns, proxy.NewCachePattern(".", time.Hour*100))
61+
fixture := newTestFixture(handler, ap)
62+
defer fixture.close()
63+
64+
resp1, err := fixture.client().Get(fixture.backend.URL)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
assertHeader(t, resp1, "Cache-Control", "max-age=360000")
70+
}
71+
72+
func TestRewritesApply(t *testing.T) {
73+
handler := func(w http.ResponseWriter, r *http.Request) {
74+
w.Write([]byte("Llamas rock"))
75+
}
76+
fixture := newTestFixture(handler, proxy.NewAptProxy())
77+
defer fixture.close()
78+
79+
resp, err := fixture.client().Get(
80+
"http://archive.ubuntu.com/ubuntu/pool/main/b/bind9/dnsutils_9.9.5.dfsg-3_amd64.deb")
81+
if err != nil {
82+
t.Fatal(err)
83+
}
84+
85+
contents, err := ioutil.ReadAll(resp.Body)
86+
if err != nil {
87+
t.Fatal(err)
88+
}
89+
90+
if bytes.Compare(contents, []byte("Llamas rock")) != 0 {
91+
t.Fatalf("Response content was incorrect, rewrites not applying?")
92+
}
93+
}

proxy/pattern.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package proxy
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"regexp"
7+
"time"
8+
)
9+
10+
// New creates a new pattern, which is a regular expression and a duration to
11+
// override the Cache-Control max-age of any responses that match
12+
func NewCachePattern(pattern string, d time.Duration) *cachePattern {
13+
return &cachePattern{Regexp: regexp.MustCompile(pattern), Duration: d}
14+
}
15+
16+
type cachePattern struct {
17+
*regexp.Regexp
18+
Duration time.Duration
19+
}
20+
21+
type CachePatternSlice []*cachePattern
22+
23+
// MatchString tries to match a given string across all patterns
24+
func (r CachePatternSlice) MatchString(subject string) (bool, *cachePattern) {
25+
for _, p := range r {
26+
if p.MatchString(subject) {
27+
return true, p
28+
}
29+
}
30+
31+
return false, nil
32+
}
33+
34+
type cachePatternTransport struct {
35+
patterns CachePatternSlice
36+
upstream http.RoundTripper
37+
}
38+
39+
func (t cachePatternTransport) RoundTrip(r *http.Request) (*http.Response, error) {
40+
resp, err := t.upstream.RoundTrip(r)
41+
if match, pattern := t.patterns.MatchString(r.URL.Path); match {
42+
resp.Header.Add("Cache-Control", fmt.Sprintf("max-age=%.f", pattern.Duration.Seconds()))
43+
}
44+
return resp, err
45+
}

proxy/proxy.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package proxy
2+
3+
import (
4+
"net/http"
5+
"net/http/httputil"
6+
7+
"github.com/lox/apt-proxy/ubuntu"
8+
)
9+
10+
type AptProxy struct {
11+
Transport http.RoundTripper
12+
Rewriters []Rewriter
13+
CachePatterns CachePatternSlice
14+
}
15+
16+
func NewAptProxy() *AptProxy {
17+
return &AptProxy{
18+
Transport: http.DefaultTransport,
19+
Rewriters: []Rewriter{ubuntu.NewRewriter()},
20+
CachePatterns: CachePatternSlice{},
21+
}
22+
}
23+
24+
func (ap *AptProxy) Handler() http.Handler {
25+
return &handler{ap: ap, Handler: &httputil.ReverseProxy{
26+
Director: func(r *http.Request) {
27+
for _, rewrite := range ap.Rewriters {
28+
rewrite.Rewrite(r)
29+
}
30+
31+
r.Host = r.URL.Host
32+
},
33+
Transport: &cachePatternTransport{ap.CachePatterns, ap.Transport},
34+
}}
35+
}
36+
37+
type handler struct {
38+
http.Handler
39+
ap *AptProxy
40+
}

proxy/rewriter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package proxy
2+
3+
import "net/http"
4+
5+
type Rewriter interface {
6+
Rewrite(req *http.Request)
7+
}
8+
9+
type RewriterFunc func(req *http.Request)
10+
11+
// Rewrite calls f(req).
12+
func (f RewriterFunc) Rewrite(req *http.Request) {
13+
f(req)
14+
}

0 commit comments

Comments
 (0)