Skip to content

Commit

Permalink
Merge pull request #5 from alpkeskin/v1.1.0
Browse files Browse the repository at this point in the history
V1.1.0
  • Loading branch information
alpkeskin authored Jan 11, 2025
2 parents f1ed910 + d766f01 commit 1448721
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 33 deletions.
12 changes: 12 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# These are supported funding model platforms

github: [alpkeskin] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
2 changes: 1 addition & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: "Set up Go"
uses: actions/setup-go@v4
with:
go-version: 1.22
go-version: 1.23.4

- name: "Create release on GitHub"
uses: goreleaser/goreleaser-action@v4.4.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches:
- main
- v1
- v*

jobs:
test:
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<a href="#installation">Installation</a> •
<a href="#configuration">Configuration</a> •
<a href="#quick-start">Quick Start</a> •
<a href="#api">API</a> •
<a href="#contributing">Contributing</a> •
<a href="#what-is-next">What's Next</a>
</p>
Expand Down Expand Up @@ -102,21 +103,31 @@ scheme://ip:port
Example:
socks5://192.111.137.37:18762
http://192.111.137.37:9911
https://192.111.137.37:9911
```

# Quick Start

```sh
rota --config config.yml
```

Default config file path is `config.yml` so you can use `rota` without any arguments.
Default config file path is `config.yml`. **So you can use `rota` without any arguments.** That's it! 🎉

### Proxy Checker
```sh
rota --config config.yml --check
```

## API

For now, API is enabled by default. You can disabled it by setting `api.enabled` to `false` in your config file.

Endpoints:
- `/healthz`: Healthcheck endpoint
- `/proxies`: Get all proxies
- `/metrics`: Get metrics


# Contributing

Expand Down
6 changes: 3 additions & 3 deletions cmd/rota/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func main() {
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)

go runFileWatcher(cfg, proxyLoader, done)
go runApi(cfg)
go runApi(cfg, proxyServer)
go proxyServer.Listen()

<-done
Expand Down Expand Up @@ -106,12 +106,12 @@ func runFileWatcher(cfg *config.Config, proxyLoader *proxy.ProxyLoader, done cha
}
}

func runApi(cfg *config.Config) {
func runApi(cfg *config.Config, proxyServer *proxy.ProxyServer) {
if !cfg.Api.Enabled {
return
}

api := api.NewApi(cfg)
api := api.NewApi(cfg, proxyServer)
err := api.Serve()
if err != nil {
slog.Error(msgFailedToServeApi, "error", err)
Expand Down
120 changes: 109 additions & 11 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,32 @@ import (
"time"

"github.com/alpkeskin/rota/internal/config"
"github.com/alpkeskin/rota/internal/proxy"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
)

const (
msgApiServerStarted = "API server started"
msgCertRequested = "cert requested"
msgFailedToCreateCert = "failed to create cert"
msgFailedToWriteCert = "failed to write cert"
msgMetricsRequested = "metrics requested"
msgMethodNotAllowed = "method not allowed"
msgFailedToCollectMetrics = "failed to collect metrics"
msgFailedToWriteMetrics = "failed to write metrics"
msgApiServerStarted = "API server started"
msgCertRequested = "cert requested"
msgFailedToCreateCert = "failed to create cert"
msgFailedToWriteCert = "failed to write cert"
msgMethodNotAllowed = "method not allowed"
msgFailedToCollectMetrics = "failed to collect metrics"
msgFailedToWriteMetrics = "failed to write metrics"
msgFailedToWriteHealthcheck = "failed to write healthcheck"
msgFailedToReadProxies = "failed to read proxies"
msgFailedToWriteProxies = "failed to write proxies"
msgHealthcheckRequested = "healthcheck requested"
msgProxiesRequested = "proxies requested"
msgMetricsRequested = "metrics requested"
)

type Api struct {
cfg *config.Config
cfg *config.Config
proxyServer *proxy.ProxyServer
startTime time.Time
}

type responseWriter struct {
Expand Down Expand Up @@ -58,13 +66,15 @@ type metrics struct {
GCPauses uint32 `json:"gc_pauses"`
}

func NewApi(cfg *config.Config) *Api {
return &Api{cfg: cfg}
func NewApi(cfg *config.Config, proxyServer *proxy.ProxyServer) *Api {
return &Api{cfg: cfg, proxyServer: proxyServer, startTime: time.Now()}
}

func (a *Api) Serve() error {
mux := http.NewServeMux()
mux.HandleFunc("/metrics", a.handleMetrics)
mux.HandleFunc("/healthz", a.handleHealthcheck)
mux.HandleFunc("/proxies", a.handleProxies)
server := &http.Server{
Addr: fmt.Sprintf(":%d", a.cfg.Api.Port),
Handler: mux,
Expand Down Expand Up @@ -114,6 +124,94 @@ func (a *Api) handleMetrics(w http.ResponseWriter, r *http.Request) {
}
}

func (a *Api) handleHealthcheck(w http.ResponseWriter, r *http.Request) {
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
w = rw

defer func() {
slog.Info(msgHealthcheckRequested,
"status", rw.statusCode,
"method", r.Method,
"url", r.URL.String(),
"ip", r.RemoteAddr,
)
}()

if r.Method != http.MethodGet {
http.Error(w, msgMethodNotAllowed, http.StatusMethodNotAllowed)
return
}

duration := time.Since(a.startTime)
uptime := fmt.Sprintf("%d days %d hours %d minutes %d seconds",
int(duration.Hours())/24,
int(duration.Hours())%24,
int(duration.Minutes())%60,
int(duration.Seconds())%60,
)
response := map[string]any{
"status": "healthy",
"timestamp": time.Now().Format(time.RFC3339),
"uptime": uptime,
"coffee": "☕",
}

w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
slog.Error(msgFailedToWriteHealthcheck, "error", err)
http.Error(w, msgFailedToWriteHealthcheck, http.StatusInternalServerError)
return
}
}

func (a *Api) handleProxies(w http.ResponseWriter, r *http.Request) {
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
w = rw

defer func() {
slog.Info(msgProxiesRequested,
"status", rw.statusCode,
"method", r.Method,
"url", r.URL.String(),
"ip", r.RemoteAddr,
)
}()

if r.Method != http.MethodGet {
http.Error(w, msgMethodNotAllowed, http.StatusMethodNotAllowed)
return
}

type proxyResponse struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
}

responses := make([]proxyResponse, len(a.proxyServer.Proxies))
for i, p := range a.proxyServer.Proxies {
responses[i] = proxyResponse{
Scheme: p.Scheme,
Host: p.Host,
}
}

jsonProxies, err := json.Marshal(responses)
if err != nil {
slog.Error(msgFailedToWriteProxies, "error", err)
http.Error(w, msgFailedToWriteProxies, http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
_, err = w.Write(jsonProxies)
if err != nil {
slog.Error(msgFailedToWriteProxies, "error", err)
http.Error(w, msgFailedToWriteProxies, http.StatusInternalServerError)
return
}
}

func collectMetrics() (*metrics, error) {
metrics := &metrics{
Timestamp: time.Now().Format(time.RFC3339),
Expand Down
42 changes: 39 additions & 3 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/alpkeskin/rota/internal/config"
"github.com/alpkeskin/rota/internal/proxy"
"github.com/stretchr/testify/assert"
)

Expand All @@ -16,8 +17,8 @@ func TestNewApi(t *testing.T) {
Port: 8081,
},
}

api := NewApi(cfg)
proxyServer := proxy.NewProxyServer(cfg)
api := NewApi(cfg, proxyServer)
assert.NotNil(t, api)
assert.Equal(t, cfg, api.cfg)
}
Expand Down Expand Up @@ -48,7 +49,8 @@ func TestHandleMetrics(t *testing.T) {
Port: 8080,
},
}
api := NewApi(cfg)
proxyServer := proxy.NewProxyServer(cfg)
api := NewApi(cfg, proxyServer)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -88,3 +90,37 @@ func TestCollectMetrics(t *testing.T) {
assert.GreaterOrEqual(t, metrics.GoRoutines, 0)
assert.GreaterOrEqual(t, metrics.ThreadCount, 0)
}

func TestHandleProxies(t *testing.T) {
cfg := &config.Config{
Api: config.ApiConfig{
Port: 8080,
},
}
proxyServer := proxy.NewProxyServer(cfg)
api := NewApi(cfg, proxyServer)

req := httptest.NewRequest(http.MethodGet, "/proxies", nil)
w := httptest.NewRecorder()

api.handleProxies(w, req)

assert.Equal(t, http.StatusOK, w.Code)
}

func TestHandleHealthcheck(t *testing.T) {
cfg := &config.Config{
Api: config.ApiConfig{
Port: 8080,
},
}
proxyServer := proxy.NewProxyServer(cfg)
api := NewApi(cfg, proxyServer)

req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
w := httptest.NewRecorder()

api.handleHealthcheck(w, req)

assert.Equal(t, http.StatusOK, w.Code)
}
16 changes: 6 additions & 10 deletions internal/proxy/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,12 @@ import (
)

const (
msgFailedToLoadProxies = "failed to load proxies"
msgLoadingProxies = "loading proxies"
msgProxiesLoadedSuccessfully = "proxies loaded successfully"
msgUnsupportedProxyScheme = "unsupported proxy scheme"
msgCheckingProxies = "checking proxies"
msgFailedToCreateOutputFile = "failed to create output file"
msgFailedToCreateRequest = "failed to create request"
msgDeadProxy = "dead proxy"
msgAliveProxy = "alive proxy"
msgFailedToWriteOutputFile = "failed to write output file"
msgCheckingProxies = "checking proxies"
msgFailedToCreateOutputFile = "failed to create output file"
msgFailedToCreateRequest = "failed to create request"
msgDeadProxy = "dead proxy"
msgAliveProxy = "alive proxy"
msgFailedToWriteOutputFile = "failed to write output file"
)

type ProxyChecker struct {
Expand Down
15 changes: 13 additions & 2 deletions internal/proxy/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import (
"h12.io/socks"
)

const (
msgFailedToCreateProxy = "failed to create proxy"
msgProxiesLoadedSuccessfully = "proxies loaded successfully"
msgLoadingProxies = "loading proxies"
msgFailedToLoadProxies = "failed to load proxies"
msgUnsupportedProxyScheme = "unsupported proxy scheme"
)

type ProxyLoader struct {
cfg *config.Config
proxyServer *ProxyServer
Expand All @@ -32,11 +40,14 @@ func (pl *ProxyLoader) Load() error {
return fmt.Errorf("%s: %w", msgFailedToLoadProxies, err)
}

lines := strings.Split(string(data), "\n")
content := strings.TrimSpace(string(data))
content = strings.ReplaceAll(content, "\r\n", "\n")
lines := strings.Split(content, "\n")
for _, line := range lines {
proxy, err := pl.CreateProxy(line)
if err != nil {
return err
slog.Error(msgFailedToCreateProxy, "error", err, "proxy", line)
continue
}

pl.proxyServer.AddProxy(proxy)
Expand Down

0 comments on commit 1448721

Please sign in to comment.