Skip to content

Commit e529848

Browse files
committed
Merge branch 'main' into encryption
2 parents 0b2ef05 + cb11c31 commit e529848

29 files changed

+883
-773
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Git Calendar Core
2-
[![Go Report Card](https://goreportcard.com/badge/github.com/firu11/git-calendar-core)](https://goreportcard.com/report/github.com/firu11/git-calendar-core)
2+
[![Go Report Card](https://goreportcard.com/badge/github.com/git-calendar/core)](https://goreportcard.com/report/github.com/git-calendar/core)
33

44

55
Related projects:
@@ -11,11 +11,7 @@ For Android and IOS bindings, make sure to install [gomobile](https://pkg.go.dev
1111
```sh
1212
go install golang.org/x/mobile/cmd/gomobile@latest
1313
```
14-
You can build for all platforms using:
15-
```sh
16-
make
17-
```
18-
or individually:
14+
You can build for specific platforms using:
1915
```sh
2016
make [build_android|build_ios|build_web]
2117
```

cmd/cors-proxy/README.md

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ podman run -d --rm \
2727

2828
### Enviroment variables (optional; those values are default)
2929
```sh
30-
CORS_PROXY_HOST=0.0.0.0
31-
CORS_PROXY_PORT=8080
32-
CORS_PROXY_PRODUCTION=false
33-
CORS_PROXY_UPSTREAM_TIMEOUT=15s
34-
CORS_PROXY_MAX_RESPONSE_SIZE=1048576 # 1MB in bytes (1024^2)
30+
HOST=0.0.0.0
31+
PORT=8080
32+
PRODUCTION=false
33+
UPSTREAM_TIMEOUT=15s
34+
MAX_RESPONSE_SIZE=1048576 # 1MB in bytes (1024^2)
35+
ALLOWED_HOSTS=github.com,raw.githubusercontent.com,gitlab.com,codeberg.org
36+
RATE_TOKENS=40
37+
RATE_INTERVAL=1m
38+
RATE_IP_SOURCE_HEADER="" # useful when behind a reverse-proxy
3539
```
3640

3741
## Usage
@@ -43,7 +47,7 @@ console.log(html);
4347
```
4448
results in:
4549
```
46-
Access to fetch at 'https://example.com' from origin 'https://...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
50+
Access to fetch at 'https://github.com' from origin 'https://...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
4751
```
4852

4953
---
@@ -64,7 +68,7 @@ succeds!
6468
## I already have a reverse-proxy
6569
When using a [bare Git repository](https://git-scm.com/book/en/v2/Git-on-the-Server-Getting-Git-on-a-Server) on a [VPS](https://en.wikipedia.org/wiki/Virtual_private_server) this proxy is not necessary, since you control the enviroment.
6670

67-
You can use any [reverse-proxy](https://en.wikipedia.org/wiki/Reverse_proxy) of your choice (e.g. [Caddy](https://caddyserver.com/) or [Nginx](https://nginx.org/en/)), and just add the CORS headers there.\
71+
You can use any [reverse-proxy](https://en.wikipedia.org/wiki/Reverse_proxy) of your choice (e.g., [Caddy](https://caddyserver.com/) or [Nginx](https://nginx.org/en/)), and just add the CORS headers there.\
6872
Example configuration for Caddy:\
6973
(based on [this](https://www.jamesatkins.com/posts/git-over-http-with-caddy/) very cool article)
7074
```caddyfile
@@ -114,6 +118,3 @@ your-repo-domain.com {
114118
}
115119
}
116120
```
117-
118-
## TODO
119-
- rate-limiter (redis? for multi instance deployment)

cmd/cors-proxy/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/git-calendar/core/cmd/cors-proxy
2+
3+
go 1.25.5
4+
5+
require (
6+
github.com/sethvargo/go-limiter v1.1.0
7+
github.com/sethvargo/go-envconfig v1.3.0
8+
)

cmd/cors-proxy/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3+
github.com/sethvargo/go-envconfig v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U=
4+
github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
5+
github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA=
6+
github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=

cmd/cors-proxy/proxy.go

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,22 @@ func main() {
3232

3333
s := &http.Server{
3434
Addr: fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
35-
Handler: accessLog(mux),
35+
Handler: accessLog(corsMiddleware(rateLimit(mux))),
3636
ReadTimeout: 10 * time.Second,
3737
WriteTimeout: 15 * time.Second,
3838
MaxHeaderBytes: 64 << 10, // 1KB
3939
}
4040

41-
slog.Info(fmt.Sprintf("configuration: %+v\n", *cfg))
41+
slog.Info(fmt.Sprintf("configuration: %+v", *cfg))
4242
slog.Info("running on " + s.Addr)
4343

4444
// run the proxy
4545
if err := s.ListenAndServe(); err != nil {
46-
slog.Error(err.Error())
4746
panic(err)
4847
}
4948
}
5049

5150
func proxyHandler(w http.ResponseWriter, r *http.Request) {
52-
// add cors headers to allow any browser to use this endpoint
53-
w.Header().Set("Access-Control-Allow-Origin", "*") // TODO add real https://git-calendar.firu.dev or whatever
54-
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
55-
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Git-Protocol")
56-
57-
if r.Method == http.MethodOptions {
58-
// this prevents the 405 from e.g. GitHub
59-
// the browser only needs to get the CORS headers and OK for OPTIONS request,
60-
// so that it knows its safe to send the real request
61-
w.WriteHeader(http.StatusNoContent)
62-
return
63-
}
64-
6551
// get the destination url from query
6652
destUrlQuery := r.URL.Query().Get("url")
6753
if destUrlQuery == "" {

cmd/cors-proxy/utils.go

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package main
22

33
import (
4+
"context"
45
"log/slog"
56
"net"
67
"net/http"
78
"net/url"
8-
"os"
99
"slices"
10-
"strconv"
1110
"strings"
1211
"time"
12+
13+
"github.com/sethvargo/go-envconfig"
14+
"github.com/sethvargo/go-limiter"
15+
"github.com/sethvargo/go-limiter/httplimit"
16+
"github.com/sethvargo/go-limiter/memorystore"
17+
"github.com/sethvargo/go-limiter/noopstore"
1318
)
1419

1520
// a transport used for destination requests
@@ -37,12 +42,65 @@ func isAllowedHost(u *url.URL) bool {
3742

3843
// ------------------- middleware -------------------
3944

45+
// Rate limits by IP.
46+
func rateLimit(next http.Handler) http.Handler {
47+
var err error
48+
var store limiter.Store
49+
50+
if cfg.Production {
51+
store, err = memorystore.New(&memorystore.Config{
52+
Tokens: cfg.RateTokens,
53+
Interval: cfg.RateInterval,
54+
})
55+
} else {
56+
store, err = noopstore.New()
57+
}
58+
if err != nil {
59+
panic(err)
60+
}
61+
62+
var limitFunc httplimit.KeyFunc
63+
if cfg.IpSourceHeader != "" {
64+
limitFunc = httplimit.IPKeyFunc(cfg.IpSourceHeader)
65+
} else {
66+
limitFunc = httplimit.IPKeyFunc()
67+
}
68+
69+
middleware, err := httplimit.NewMiddleware(store, limitFunc)
70+
if err != nil {
71+
panic(err)
72+
}
73+
74+
return middleware.Handle(next)
75+
}
76+
77+
// Adds CORS headers to allow any browser to use this endpoint.
78+
func corsMiddleware(next http.Handler) http.Handler {
79+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
80+
w.Header().Set("Access-Control-Allow-Origin", "*") // TODO: add real https://git-calendar.firu.dev or whatever
81+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
82+
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Git-Protocol")
83+
84+
// handle preflight OPTIONS request
85+
if r.Method == http.MethodOptions {
86+
// this prevents the 405 from e.g., GitHub
87+
// the browser only needs to get the CORS headers and OK for OPTIONS request,
88+
// so that it knows it's safe to send the real request
89+
w.WriteHeader(http.StatusNoContent)
90+
return
91+
}
92+
93+
next.ServeHTTP(w, r) // next handler
94+
})
95+
}
96+
97+
// Logs access after execution.
4098
func accessLog(next http.Handler) http.Handler {
4199
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42100
start := time.Now() // start timer
43101

44102
ww := &responseWriter{ResponseWriter: w, status: http.StatusOK} // wrap the writer into our custom one
45-
next.ServeHTTP(ww, r) // execute handler
103+
next.ServeHTTP(ww, r) // next handler
46104

47105
duration := time.Since(start) // stop timer
48106

@@ -54,7 +112,7 @@ func accessLog(next http.Handler) http.Handler {
54112
})
55113
}
56114

57-
// a http.ResponseWriter wrapper, which catches the status code for logging
115+
// A http.ResponseWriter wrapper, which catches the status code for logging.
58116
type responseWriter struct {
59117
http.ResponseWriter
60118
status int
@@ -105,55 +163,23 @@ func removeHopByHopHeaders(h http.Header) {
105163

106164
// ------------------- config -------------------
107165

108-
// External library? https://github.com/caarlos0/env
109-
// Overkill! (for now)
110-
111-
const prefix = "CORS_PROXY_"
112-
113166
type config struct {
114-
Host string
115-
Port string
116-
Production bool
117-
UpstreamTimeout time.Duration
118-
MaxResponseSize int64
119-
AllowedHosts []string
167+
Host string `env:"HOST,default=0.0.0.0"`
168+
Port string `env:"PORT,default=8080"`
169+
Production bool `env:"PRODUCTION,default=false"`
170+
UpstreamTimeout time.Duration `env:"UPSTREAM_TIMEOUT,default=15s"`
171+
MaxResponseSize int64 `env:"MAX_RESPONSE_SIZE,default=1048576"` // 1MiB
172+
AllowedHosts []string `env:"ALLOWED_HOSTS,default=github.com,raw.githubusercontent.com,gitlab.com,codeberg.org"`
173+
RateTokens uint64 `env:"RATE_TOKENS,default=40"` // 40 req/min should be ok for legit usage
174+
RateInterval time.Duration `env:"RATE_INTERVAL,default=1m"`
175+
IpSourceHeader string `env:"RATE_IP_SOURCE_HEADER"` // for reverse proxy etc.
120176
}
121177

122178
func loadConfig() {
123-
cfg = &config{}
124-
var err error
125-
126-
prodEnv := os.Getenv(prefix + "PRODUCTION")
127-
cfg.Production = prodEnv == "true" || prodEnv == "1" || prodEnv == "True" || prodEnv == "TRUE"
128-
129-
cfg.Host = os.Getenv(prefix + "HOST") // empty is the same as 0.0.0.0
130-
cfg.Port = os.Getenv(prefix + "PORT")
131-
if cfg.Port == "" {
132-
cfg.Port = "8080"
133-
}
134-
135-
cfg.UpstreamTimeout, err = time.ParseDuration(os.Getenv(prefix + "UPSTREAM_TIMEOUT"))
136-
if err != nil || cfg.MaxResponseSize == 0 {
137-
cfg.UpstreamTimeout = 15 * time.Second
138-
}
139-
140-
cfg.MaxResponseSize, err = strconv.ParseInt(os.Getenv(prefix+"MAX_RESPONSE_SIZE"), 10, 64)
141-
if err != nil || cfg.MaxResponseSize == 0 {
142-
cfg.MaxResponseSize = 1 << 20 // 1MB
143-
}
179+
cfg = envconfig.MustProcess(context.Background(), &config{})
144180

145-
rawHostsEnv := os.Getenv(prefix + "ALLOWED_HOSTS")
146-
if len(rawHostsEnv) == 0 {
147-
cfg.AllowedHosts = []string{
148-
"github.com",
149-
"raw.githubusercontent.com",
150-
"gitlab.com",
151-
"codeberg.org",
152-
}
153-
} else {
154-
cfg.AllowedHosts = strings.Split(os.Getenv(prefix+"ALLOWED_HOSTS"), ",")
155-
for i := range cfg.AllowedHosts {
156-
cfg.AllowedHosts[i] = strings.TrimSpace(cfg.AllowedHosts[i]) // remove extra spaces: " github.com"
157-
}
181+
// trim spaces for hosts
182+
for i := range cfg.AllowedHosts {
183+
cfg.AllowedHosts[i] = strings.TrimSpace(cfg.AllowedHosts[i])
158184
}
159185
}

cmd/wasm/main.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package main
55
import (
66
"syscall/js"
77

8-
"github.com/firu11/git-calendar-core/pkg/api"
8+
"github.com/git-calendar/core/pkg/api"
99
)
1010

1111
// This is the starting point which gets called from JS.
@@ -85,9 +85,14 @@ func RegisterCallbacks(api *api.Api) {
8585
return api.UpdateEvent(args[0].String())
8686
})
8787
}),
88-
"updateEventWithStrategy": js.FuncOf(func(this js.Value, args []js.Value) any {
88+
"updateRepeatingEvent": js.FuncOf(func(this js.Value, args []js.Value) any {
8989
return wrapPromise(func() (any, error) {
90-
return api.UpdateEventWithStrategy(args[0].String(), args[1].Int())
90+
return api.UpdateRepeatingEvent(args[0].String(), args[1].String(), args[2].Int())
91+
})
92+
}),
93+
"removeRepeatingEvent": js.FuncOf(func(this js.Value, args []js.Value) any {
94+
return wrapPromise(func() (any, error) {
95+
return nil, api.RemoveRepeatingEvent(args[0].String(), args[1].Int())
9196
})
9297
}),
9398
// TODO others

0 commit comments

Comments
 (0)