diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index daf4e97..ae2aabe 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,5 +1,8 @@ name: 🔨 Build Test on: + push: + branches: + - master pull_request: workflow_dispatch: @@ -34,10 +37,6 @@ jobs: working-directory: cmd/notify/ - name: Integration Tests - env: - DISCORD_WEBHOOK_URL: "${{ secrets.DISCORD_WEBHOOK_URL }}" - SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}" - CUSTOM_WEBHOOK_URL: "${{ secrets.CUSTOM_WEBHOOK_URL }}" run: | chmod +x gotify.sh chmod +x action-run.sh diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 9f533f8..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: 🚨 CodeQL Analysis - -on: - workflow_dispatch: - pull_request: - branches: - - dev - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'go' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/dockerhub-push.yml b/.github/workflows/dockerhub-push.yml deleted file mode 100644 index 2fb3ef9..0000000 --- a/.github/workflows/dockerhub-push.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: 🌥 Docker Push - -on: - workflow_run: - workflows: ["🎉 Release Binary"] - types: - - completed - workflow_dispatch: - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Git Checkout - uses: actions/checkout@v3 - - - name: Get Github tag - id: meta - run: | - curl --silent "https://api.github.com/repos/projectdiscovery/notify/releases/latest" | jq -r .tag_name | xargs -I {} echo TAG={} >> $GITHUB_OUTPUT - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to DockerHub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v4 - with: - context: . - platforms: linux/amd64,linux/arm64,linux/arm - push: true - tags: projectdiscovery/notify:latest,projectdiscovery/notify:${{ steps.meta.outputs.tag }} \ No newline at end of file diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index 57d31cf..d6db105 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -1,6 +1,9 @@ name: 🙏🏻 Lint Test on: + push: + branches: + - master pull_request: paths: - '**.go' diff --git a/.github/workflows/merge_upstream.yml b/.github/workflows/merge_upstream.yml new file mode 100644 index 0000000..8fdd52d --- /dev/null +++ b/.github/workflows/merge_upstream.yml @@ -0,0 +1,21 @@ +name: auto update + +on: + schedule: + - cron: "0 12 * * *" + workflow_dispatch: + inputs: {} + +jobs: + autoupdate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.PAT }} + - uses: fopina/upstream-to-pr@v1 + with: + token: ${{ secrets.PAT }} + upstream-repository: https://github.com/projectdiscovery/notify + upstream-tag: v\d+\.\d+\.\d+ diff --git a/.github/workflows/release-binary.yml b/.github/workflows/release-binary.yml index 9f7f247..de052d0 100644 --- a/.github/workflows/release-binary.yml +++ b/.github/workflows/release-binary.yml @@ -20,6 +20,12 @@ jobs: with: go-version: 1.21.x + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: "Create release on GitHub" uses: goreleaser/goreleaser-action@v4 with: @@ -28,6 +34,3 @@ jobs: workdir: . env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - SLACK_WEBHOOK: "${{ secrets.RELEASE_SLACK_WEBHOOK }}" - DISCORD_WEBHOOK_ID: "${{ secrets.DISCORD_WEBHOOK_ID }}" - DISCORD_WEBHOOK_TOKEN: "${{ secrets.DISCORD_WEBHOOK_TOKEN }}" \ No newline at end of file diff --git a/.goreleaser.yml b/.goreleaser.yml index bb26ed6..983f753 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,6 +15,9 @@ builds: - arm - arm64 + goarm: + - '7' + ignore: - goos: darwin goarch: '386' @@ -31,13 +34,35 @@ archives: checksum: algorithm: sha256 -announce: - slack: - enabled: true - channel: '#release' - username: GoReleaser - message_template: 'New Release: {{ .ProjectName }} {{.Tag}} is published! Check it out at {{ .ReleaseURL }}' +dockers: + - &image-def + image_templates: + - fopina/{{.ProjectName}}:{{ .Version }}-amd64 + use: buildx + goos: linux + goarch: amd64 + build_flag_templates: + - --platform=linux/amd64 + - <<: *image-def + image_templates: + - fopina/{{.ProjectName}}:{{ .Version }}-arm64 + goarch: arm64 + build_flag_templates: + - --platform=linux/arm64/v8 + - <<: *image-def + image_templates: + - fopina/{{.ProjectName}}:{{ .Version }}-armv7 + goarch: arm + goarm: '7' + build_flag_templates: + - --platform=linux/arm/v7 - discord: - enabled: true - message_template: '**New Release: {{ .ProjectName }} {{.Tag}}** is published! Check it out at {{ .ReleaseURL }}' \ No newline at end of file +docker_manifests: + - &manifest-def + name_template: fopina/{{.ProjectName}}:{{ .Version }} + image_templates: + - fopina/{{.ProjectName}}:{{ .Version }}-armv7 + - fopina/{{.ProjectName}}:{{ .Version }}-arm64 + - fopina/{{.ProjectName}}:{{ .Version }}-amd64 + - <<: *manifest-def + name_template: fopina/{{.ProjectName}}:latest diff --git a/Dockerfile b/Dockerfile index 96ed548..6ab6c24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,5 @@ -# Base -FROM golang:1.21.4-alpine AS builder -RUN apk add --no-cache build-base -WORKDIR /app -COPY . /app -RUN go mod download -RUN go build ./cmd/notify +FROM alpine:latest -# Release -FROM alpine:3.18.2 -RUN apk -U upgrade --no-cache \ - && apk add --no-cache bind-tools ca-certificates -COPY --from=builder /app/notify /usr/local/bin/ +COPY notify /usr/local/bin/notify ENTRYPOINT ["notify"] \ No newline at end of file diff --git a/README.md b/README.md index c0f412f..ce4565c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,42 @@ Notify is a Go-based assistance package that enables you to stream the output of
+# THIS FORK + +This fork implements a `-web` flag (until a it is cleaned up and a PR can be created) + +## Web + +In some restricted (container?) environments it might be useful to have a single notify installation (and configuration) available for multiple services/scripts to use it. + +`-web` starts a local webserver and anything POSTed to that URL will be pushed as if it was stdin. + +``` +$ notify -web +Up and running! + +Post raw data to http://127.0.0.1:8888/, as in: + + curl http://127.0.0.1:8888/ -d 'testing 1 2 3' + +This will send that data as message using all profiles. +To use a specific one, post to http://127.0.0.1:8888/PROFILE +``` + +## Docker + +`-web` mode is specially useful in a cluster of containers so that notify does not need to be installed and configured in every image. + +An image is ready to be used in [the hub](https://hub.docker.com/r/fopina/notify): + +``` +$ docker run --rm \ + -v ~/.providers.conf:/.providers.conf:ro \ + -p 8888:8888 \ + fopina/notify \ + -w -b 0.0.0.0:8888 -pc /.providers.conf +``` + # Features - Supports for Slack / Discord / Telegram diff --git a/cmd/notify/notify.go b/cmd/notify/notify.go index 52783a5..aec3af3 100644 --- a/cmd/notify/notify.go +++ b/cmd/notify/notify.go @@ -39,7 +39,11 @@ func main() { }() }() - err = notifyRunner.Run() + if options.Web { + err = notifyRunner.StartWeb() + } else { + err = notifyRunner.Run() + } if err != nil { gologger.Fatal().Msgf("Could not run notifier: %s\n", err) } @@ -66,6 +70,8 @@ func readConfig() { set.StringVar(&options.Proxy, "proxy", "", "HTTP Proxy to use with notify") set.CallbackVarP(runner.GetUpdateCallback(), "update", "up", "update notify to latest version") set.BoolVarP(&options.DisableUpdateCheck, "disable-update-check", "duc", false, "disable automatic notify update check") + set.StringVar(&options.WebBind, "web-bind", "127.0.0.1:8888", "Address and port to bind web server") + set.BoolVar(&options.Web, "web", false, "Run as webserver, using raw POST data as message") _ = set.Parse() diff --git a/internal/runner/runner.go b/internal/runner/runner.go index c4ff399..6bb45ff 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -3,6 +3,7 @@ package runner import ( "bufio" "crypto/tls" + "fmt" "io" "log" "net/http" @@ -24,8 +25,9 @@ import ( // Runner contains the internal logic of the program type Runner struct { - options *types.Options - providers *providers.Client + options *types.Options + providers *providers.Client + providerOptions *providers.ProviderOptions } // NewRunner instance @@ -57,7 +59,7 @@ func NewRunner(options *types.Options) (*Runner, error) { return nil, err } - return &Runner{options: options, providers: prClient}, nil + return &Runner{options: options, providers: prClient, providerOptions: &providerOptions}, nil } // Run polling and notification @@ -126,6 +128,88 @@ func (r *Runner) Run() error { return br.Err() } +// Run polling and notification +func (r *Runner) StartWeb() error { + defaultTransport := http.DefaultTransport.(*http.Transport) + if r.options.Proxy != "" { + proxyurl, err := url.Parse(r.options.Proxy) + if err != nil || proxyurl == nil { + gologger.Warning().Msgf("supplied proxy '%s' is not valid", r.options.Proxy) + } else { + defaultTransport = &http.Transport{ + Proxy: http.ProxyURL(proxyurl), + ForceAttemptHTTP2: true, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + } + } + + if r.options.RateLimit > 0 { + http.DefaultClient.Transport = utils.NewThrottledTransport(time.Second, r.options.RateLimit, defaultTransport) + } + + var err error + var splitter bufio.SplitFunc + + splitter, err = bulkSplitter(r.options.CharLimit) + if err != nil { + return err + } + + handler := func(w http.ResponseWriter, req *http.Request) { + br := bufio.NewScanner(req.Body) + + if r.options.CharLimit > bufio.MaxScanTokenSize { + // Satisfy the condition of our splitters, which is that charLimit is <= the size of the bufio.Scanner buffer + buffer := make([]byte, 0, r.options.CharLimit) + br.Buffer(buffer, r.options.CharLimit) + } + br.Split(splitter) + + p := req.URL.Path[1:] + client := r.providers + if p != "" { + newOptions := *r.options + newOptions.IDs = []string{p} + prClient, err := providers.New(r.providerOptions, &newOptions) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "%v", err) + return + } + client = prClient + } + for br.Scan() { + msg := br.Text() + if len(msg) > 0 { + err := client.Send(msg) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "%v", err) + return + } + } + } + err = br.Err() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(w, "%v", err) + return + } + fmt.Fprintf(w, "ok") + } + gologger.Info().Msgf(`Up and running! +Post raw data to http://%s/, as in: + curl http://%s/ -d 'testing 1 2 3' +This will send that data as message using all profiles. +To use a specific one, post to http://%s/PROFILE +`, r.options.WebBind, r.options.WebBind, r.options.WebBind) + http.HandleFunc("/", handler) + return http.ListenAndServe(r.options.WebBind, nil) +} + func (r *Runner) sendMessage(msg string) error { if len(msg) > 0 { if r.options.Delay > 0 { diff --git a/pkg/providers/providers.go b/pkg/providers/providers.go index f90856c..ec24584 100644 --- a/pkg/providers/providers.go +++ b/pkg/providers/providers.go @@ -1,6 +1,8 @@ package providers import ( + "fmt" + "github.com/acarl005/stripansi" "github.com/pkg/errors" "go.uber.org/multierr" @@ -46,6 +48,7 @@ type Client struct { func New(providerOptions *ProviderOptions, options *types.Options) (*Client, error) { client := &Client{providerOptions: providerOptions, options: options} + totalProviders := 0 if providerOptions.Slack != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "slack")) { @@ -55,6 +58,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err } client.providers = append(client.providers, provider) + totalProviders += len(provider.Slack) } if providerOptions.Discord != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "discord")) { @@ -63,6 +67,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create discord provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.Discord) } if providerOptions.Pushover != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "pushover")) { @@ -71,6 +76,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create pushover provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.Pushover) } if providerOptions.GoogleChat != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "googlechat")) { @@ -87,6 +93,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create smtp provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.SMTP) } if providerOptions.Teams != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "teams")) { @@ -95,6 +102,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create teams provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.Teams) } if providerOptions.Telegram != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "telegram")) { @@ -103,6 +111,7 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create telegram provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.Telegram) } if providerOptions.Custom != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "custom")) { @@ -112,6 +121,11 @@ func New(providerOptions *ProviderOptions, options *types.Options) (*Client, err return nil, errors.Wrap(err, "could not create custom provider client") } client.providers = append(client.providers, provider) + totalProviders += len(provider.Custom) + } + + if totalProviders == 0 { + return nil, fmt.Errorf("no providers matching %v", options.IDs) } if providerOptions.Gotify != nil && (len(options.Providers) == 0 || sliceutil.Contains(options.Providers, "gotify")) { diff --git a/pkg/types/types.go b/pkg/types/types.go index 3656411..a598b49 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -13,6 +13,8 @@ type Options struct { Proxy string `yaml:"proxy,omitempty"` RateLimit int `yaml:"rate_limit,omitempty"` Delay int `yaml:"delay,omitempty"` + Web bool `yaml:"web,omitempty"` + WebBind string `yaml:"web_bind,omitempty"` MessageFormat string `yaml:"message_format,omitempty"`