diff --git a/gitea_status_proxy/config.go b/gitea_status_proxy/config.go new file mode 100644 index 0000000..2ca8cc8 --- /dev/null +++ b/gitea_status_proxy/config.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "github.com/tailscale/hujson" +) + +type Config struct { + ForgeEndpoint string `json:"forge_url"` + Keys []string `json:"keys"` +} + +type contextKey string + +const configKey contextKey = "config" + +func ReadConfig(reader io.Reader) (*Config, error) { + data, err := io.ReadAll(reader) + if err != nil { + return nil, fmt.Errorf("error reading config data: %w", err) + } + config := Config{} + data, err = hujson.Standardize(data) + if err != nil { + return nil, fmt.Errorf("failed to parse json: %w", err) + } + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("error parsing json to api keys and target url: %w", err) + } + + return &config, nil +} + +func ReadConfigFile(filename string) (*Config, error) { + file, err := os.Open(filename) + if err != nil { + return nil, fmt.Errorf("cannot open config file for reading. err: %w", err) + } + defer file.Close() + + return ReadConfig(file) +} diff --git a/gitea_status_proxy/handlers.go b/gitea_status_proxy/handlers.go new file mode 100644 index 0000000..35b333c --- /dev/null +++ b/gitea_status_proxy/handlers.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + "net/http" +) + +func ConfigMiddleWare(cfg *Config) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), configKey, cfg) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/gitea_status_proxy/main.go b/gitea_status_proxy/main.go new file mode 100644 index 0000000..3ac6b0b --- /dev/null +++ b/gitea_status_proxy/main.go @@ -0,0 +1,169 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + + "src.opensuse.org/autogits/common" +) + +type Status struct { + Context string `json:"context"` + State string `json:"state"` + TargetUrl string `json:"target_url"` +} + +type StatusInput struct { + State string `json:"state"` + TargetUrl string `json:"target_url"` +} + +func main() { + configFile := flag.String("config", "", "status proxy config file") + flag.Parse() + + if *configFile == "" { + common.LogError("missing required argument config") + return + } + + config, err := ReadConfigFile(*configFile) + + if err != nil { + common.LogError("Failed to read config file", err) + return + } + + mux := http.NewServeMux() + + mux.Handle("/repos/{owner}/{repo}/statuses/{sha}", ConfigMiddleWare(config)(http.HandlerFunc(StatusProxy))) + + common.LogInfo("server up and listening on :3000") + err = http.ListenAndServe(":3000", mux) + + if err != nil { + common.LogError("Server failed to start up", err) + } + +} + +func StatusProxy(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + config, ok := r.Context().Value(configKey).(*Config) + + if !ok { + common.LogError("Config missing from context") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + header := r.Header.Get("Authorization") + if header == "" { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + token_arr := strings.Split(header, " ") + if len(token_arr) != 2 { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + if !strings.EqualFold(token_arr[0], "Bearer") { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + token := token_arr[1] + + if !slices.Contains(config.Keys, token) { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + owner := r.PathValue("owner") + repo := r.PathValue("repo") + sha := r.PathValue("sha") + + if !ok { + common.LogError("Failed to get config from context, is it set?") + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + } + + posturl := fmt.Sprintf("%s/repos/%s/%s/statuses/%s", config.ForgeEndpoint, owner, repo, sha) + decoder := json.NewDecoder(r.Body) + var statusinput StatusInput + err := decoder.Decode(&statusinput) + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + status := Status{ + Context: "Build in obs", + State: statusinput.State, + TargetUrl: statusinput.TargetUrl, + } + + status_payload, err := json.Marshal(status) + + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + client := &http.Client{} + req, err := http.NewRequest("POST", posturl, bytes.NewBuffer(status_payload)) + + if err != nil { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + ForgeToken := os.Getenv("GITEA_TOKEN") + + if ForgeToken == "" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + common.LogError("GITEA_TOKEN was not set, all requests will fail") + return + } + + req.Header.Add("Content-Type", "Content-Type") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", ForgeToken)) + + resp, err := client.Do(req) + + if err != nil { + common.LogError(fmt.Sprintf("Request to forge endpoint failed: %v", err)) + http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(resp.StatusCode) + + /* + the commented out section sets every key + value from the headers, unsure if this + leaks information from gitea + + for k, v := range resp.Header { + for _, vv := range v { + w.Header().Add(k, vv) + } + } + */ + + _, err = io.Copy(w, resp.Body) + if err != nil { + common.LogError("Error copying response body: %v", err) + } + } else { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } +}