diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..274acd1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM golang:1.26 AS builder + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + gcc \ + libc6-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -ldflags="-linkmode external -extldflags '-static' -s -w" -o oauth2-proxy . + +FROM scratch + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=builder /app/oauth2-proxy /oauth2-proxy + +EXPOSE 5000 + +ENTRYPOINT ["/oauth2-proxy"] diff --git a/Justfile b/Justfile index e5f0f39..cd39a9a 100644 --- a/Justfile +++ b/Justfile @@ -1,4 +1,10 @@ set dotenv-load := true dev: - CLIENT_ID=$CLIENT_ID CLIENT_SECRET=$CLIENT_SECRET REDIRECT_URI=$REDIRECT_URI AUTH_CONFIG=$AUTH_CONFIG go run . + go run . + +build tag="latest": + docker build -t git.zatch.ru/tsivinsky/oauth2-proxy:{{tag}} . + +push tag="latest": + docker push git.zatch.ru/tsivinsky/oauth2-proxy:{{tag}} diff --git a/config.go b/config.go new file mode 100644 index 0000000..8fc8d6f --- /dev/null +++ b/config.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "golang.org/x/oauth2" +) + +func readConfig(filePath string) (*oauth2.Token, error) { + if _, err := os.Stat(filePath); os.IsNotExist(err) { + f, err := os.Create(filePath) + if err != nil { + return nil, fmt.Errorf("failed to create config file: %v", err) + } + if _, err := f.Write([]byte("{}")); err != nil { + return nil, fmt.Errorf("failed to write empty object: %v", err) + } + return &oauth2.Token{}, nil + } + + f, err := os.OpenFile(filePath, os.O_RDWR, 0644) + if err != nil { + return nil, fmt.Errorf("failed to open config file: %v", err) + } + defer f.Close() + + token := new(oauth2.Token) + if err := json.NewDecoder(f).Decode(token); err != nil { + return nil, fmt.Errorf("failed to decode json from config file: %v", err) + } + + return token, nil +} + +func writeConfig(filePath string, token *oauth2.Token) error { + f, err := os.OpenFile(filePath, os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open config file: %v", err) + } + defer f.Close() + + if err := json.NewEncoder(f).Encode(token); err != nil { + return fmt.Errorf("failed to encode json to config file: %v", err) + } + + return nil +} diff --git a/go.mod b/go.mod index d6e69c0..9f6f1e8 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module raindrop-glance +module oauth2-proxy go 1.25.0 + +require golang.org/x/oauth2 v0.35.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dab0662 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= diff --git a/main.go b/main.go index f0e9604..2b9559e 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,15 @@ package main import ( - "encoding/json" + "context" "fmt" "io" "log" "net/http" "os" - "raindrop-glance/raindrop" -) -func sendJSON(w http.ResponseWriter, data any, status int) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - if err := json.NewEncoder(w).Encode(data); err != nil { - http.Error(w, err.Error(), 500) - } -} + "golang.org/x/oauth2" +) func main() { authConfigPath := os.Getenv("AUTH_CONFIG") @@ -24,72 +17,47 @@ func main() { log.Fatalf("AUTH_CONFIG env is missing") } - if _, err := os.Stat(authConfigPath); os.IsNotExist(err) { - os.Create(authConfigPath) - os.WriteFile(authConfigPath, []byte("{}"), 0644) - } - - data, err := os.ReadFile(authConfigPath) + token, err := readConfig(authConfigPath) if err != nil { - log.Fatalf("failed to read auth config file: %v\n", err) + log.Fatalf("failed to read config file: %v\n", err) } - config := new(raindrop.TokensResponse) - if err := json.Unmarshal(data, config); err != nil { - log.Fatalf("failed to decode auth config file: %v\n", err) - } - - rdClient, err := raindrop.NewClient(raindrop.ClientConfig{ - ClientId: os.Getenv("CLIENT_ID"), + oauthConfig := oauth2.Config{ + ClientID: os.Getenv("CLIENT_ID"), ClientSecret: os.Getenv("CLIENT_SECRET"), - RedirectURI: os.Getenv("REDIRECT_URI"), - }) - if err != nil { - log.Fatalf("failed to create raindrop client: %v\n", err) - } - - if config.AccessToken != "" { - rdClient.SetApiToken(config.TokenType, config.AccessToken) + Endpoint: oauth2.Endpoint{ + AuthURL: os.Getenv("AUTH_URL"), + TokenURL: os.Getenv("TOKEN_URL"), + DeviceAuthURL: os.Getenv("DEVICE_AUTH_URL"), + AuthStyle: oauth2.AuthStyleAutoDetect, + }, + RedirectURL: os.Getenv("REDIRECT_URI"), } mux := http.NewServeMux() - mux.HandleFunc("GET /login", func(w http.ResponseWriter, r *http.Request) { - rdClient.OauthRedirect(w, r) + mux.HandleFunc("GET /oauth", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, oauthConfig.AuthCodeURL(""), http.StatusTemporaryRedirect) }) - mux.HandleFunc("GET /oauth/redirect", func(w http.ResponseWriter, r *http.Request) { - code := r.URL.Query().Get("code") - if code == "" { - http.Error(w, "invalid request", 400) - return - } - - resp, err := rdClient.ExchangeOauthCode(code) + mux.HandleFunc("GET /oauth/callback", func(w http.ResponseWriter, r *http.Request) { + token, err = oauthConfig.Exchange(context.Background(), r.URL.Query().Get("code")) if err != nil { http.Error(w, err.Error(), 500) return } - config = resp - jsonData, err := json.MarshalIndent(resp, "", " ") - if err != nil { - http.Error(w, fmt.Sprintf("failed to encode config file: %v", err), 500) + if err := writeConfig(authConfigPath, token); err != nil { + http.Error(w, err.Error(), 500) return } - if err := os.WriteFile(authConfigPath, jsonData, 0644); err != nil { - http.Error(w, fmt.Sprintf("failed to save config file: %v", err), 500) - return - } - - rdClient.SetApiToken(resp.TokenType, resp.AccessToken) w.WriteHeader(200) fmt.Fprintf(w, "ok") }) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - baseURL := "https://api.raindrop.io/rest/v1" + baseURL := os.Getenv("API_URL") req, err := http.NewRequest(r.Method, baseURL+r.URL.Path, r.Body) if err != nil { http.Error(w, fmt.Sprintf("failed to create request: %v", err), 500) @@ -97,8 +65,8 @@ func main() { } req.URL.RawQuery = r.URL.RawQuery - if config.AccessToken != "" { - authHeader := fmt.Sprintf("%s %s", config.TokenType, config.AccessToken) + if token.AccessToken != "" { + authHeader := fmt.Sprintf("%s %s", token.TokenType, token.AccessToken) req.Header.Add("Authorization", authHeader) } @@ -118,15 +86,6 @@ func main() { if _, err := io.Copy(w, resp.Body); err != nil { http.Error(w, err.Error(), 500) } - - // data, err, status := rdClient.MakeApiRequest(r.Method, r.URL.Path, r.Body, r.URL.Query()) - // if err != nil { - // http.Error(w, err.Error(), status) - // return - // } - // w.Header().Set("Content-Type", "application/json") - // w.WriteHeader(status) - // w.Write(data) }) log.Println("starting http server") diff --git a/raindrop/api.go b/raindrop/api.go deleted file mode 100644 index 9fdd259..0000000 --- a/raindrop/api.go +++ /dev/null @@ -1,42 +0,0 @@ -package raindrop - -import ( - "fmt" - "io" - "net/http" - "net/url" -) - -func (c *Client) MakeApiRequest(method string, path string, body io.Reader, params url.Values) ([]byte, error, int) { - u, err := url.Parse(c.getApiURL(path)) - if err != nil { - return nil, fmt.Errorf("failed to parse url: %v", err), 500 - } - u.RawQuery = params.Encode() - - req, err := http.NewRequest(method, u.String(), body) - if err != nil { - return nil, fmt.Errorf("failed to create request: %v", err), 500 - } - - if c.token != nil { - req.Header.Set("Authorization", c.token.Type+" "+c.token.Value) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to send request: %v", err), resp.StatusCode - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read data from response: %v", err), 500 - } - - if resp.StatusCode >= 400 { - return data, fmt.Errorf("response failed with status %s", resp.Status), resp.StatusCode - } - - return data, nil, resp.StatusCode -} diff --git a/raindrop/auth.go b/raindrop/auth.go deleted file mode 100644 index 3ce91ab..0000000 --- a/raindrop/auth.go +++ /dev/null @@ -1,68 +0,0 @@ -package raindrop - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/url" -) - -func (c *Client) OauthRedirect(w http.ResponseWriter, r *http.Request) { - authUrl, _ := url.Parse("https://raindrop.io/oauth/authorize") - params := url.Values{} - params.Set("redirect_uri", c.config.RedirectURI) - params.Set("client_id", c.config.ClientId) - params.Set("response_type", "code") - authUrl.RawQuery = params.Encode() - - http.Redirect(w, r, authUrl.String(), http.StatusFound) -} - -type TokensResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` - // token lifetime in seconds - ExpiresIn int `json:"expires_in"` -} - -func (c *Client) ExchangeOauthCode(code string) (*TokensResponse, error) { - body := struct { - GrantType string `json:"grant_type"` - Code string `json:"code"` - ClientId string `json:"client_id"` - ClientSecret string `json:"client_secret"` - RedirectURI string `json:"redirect_uri"` - }{ - GrantType: "authorization_code", - Code: code, - ClientId: c.config.ClientId, - ClientSecret: c.config.ClientSecret, - RedirectURI: c.config.RedirectURI, - } - data, err := json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("failed to encode data to send in request: %v", err) - } - - resp, err := http.Post("https://raindrop.io/oauth/access_token", "application/json", bytes.NewReader(data)) - if err != nil { - return nil, fmt.Errorf("failed to send request to raindrop api: %v", err) - } - defer resp.Body.Close() - - response := &TokensResponse{} - if err := json.NewDecoder(resp.Body).Decode(response); err != nil { - return nil, fmt.Errorf("failed to decode response from raindrop api: %v", err) - } - - return response, nil -} - -func (c *Client) SetApiToken(tokenType string, value string) { - c.token = &ApiToken{ - Type: tokenType, - Value: value, - } -} diff --git a/raindrop/client.go b/raindrop/client.go deleted file mode 100644 index 8478be3..0000000 --- a/raindrop/client.go +++ /dev/null @@ -1,36 +0,0 @@ -package raindrop - -import "fmt" - -type ClientConfig struct { - ClientId string - ClientSecret string - RedirectURI string -} - -type ApiToken struct { - Type string - Value string -} - -type Client struct { - config ClientConfig - token *ApiToken -} - -func NewClient(config ClientConfig) (*Client, error) { - if config.ClientId == "" || config.ClientSecret == "" || config.RedirectURI == "" { - return nil, fmt.Errorf("some environment variables missing") - } - return &Client{ - config: config, - }, nil -} - -func (c *Client) baseURL() string { - return "https://api.raindrop.io/rest/v1" -} - -func (c *Client) getApiURL(path string) string { - return c.baseURL() + path -}