about summary refs log tree commit diff
path: root/lib/mastodon/ip_blocks_cli.rb
blob: 08939c09267606a01fc47cddc1058e6beb995c85 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# frozen_string_literal: true

require 'rubygems/package'
require_relative '../../config/boot'
require_relative '../../config/environment'
require_relative 'cli_helper'

module Mastodon
  class IpBlocksCLI < Thor
    def self.exit_on_failure?
      true
    end

    option :severity, required: true, enum: %w(no_access sign_up_requires_approval), desc: 'Severity of the block'
    option :comment, aliases: [:c], desc: 'Optional comment'
    option :duration, aliases: [:d], type: :numeric, desc: 'Duration of the block in seconds'
    option :force, type: :boolean, aliases: [:f], desc: 'Overwrite existing blocks'
    desc 'add IP...', 'Add one or more IP blocks'
    long_desc <<-LONG_DESC
      Add one or more IP blocks. You can use CIDR syntax to
      block IP ranges. You must specify --severity of the block. All
      options will be copied for each IP block you create in one command.

      You can add a --comment. If an IP block already exists for one of
      the provided IPs, it will be skipped unless you use the --force
      option to overwrite it.
    LONG_DESC
    def add(*addresses)
      if addresses.empty?
        say('No IP(s) given', :red)
        exit(1)
      end

      skipped   = 0
      processed = 0
      failed    = 0

      addresses.each do |address|
        ip_block = IpBlock.find_by(ip: address)

        if ip_block.present? && !options[:force]
          say("#{address} is already blocked", :yellow)
          skipped += 1
          next
        end

        ip_block ||= IpBlock.new(ip: address)

        ip_block.severity   = options[:severity]
        ip_block.comment    = options[:comment] if options[:comment].present?
        ip_block.expires_in = options[:duration]

        if ip_block.save
          processed += 1
        else
          say("#{address} could not be saved", :red)
          failed += 1
        end
      end

      say("Added #{processed}, skipped #{skipped}, failed #{failed}", color(processed, failed))
    end

    option :force, type: :boolean, aliases: [:f], desc: 'Remove blocks for ranges that cover given IP(s)'
    desc 'remove IP...', 'Remove one or more IP blocks'
    long_desc <<-LONG_DESC
      Remove one or more IP blocks. Normally, only exact matches are removed. If
      you want to ensure that all of the given IP addresses are unblocked, you
      can use --force which will also remove any blocks for IP ranges that would
      cover the given IP(s).
    LONG_DESC
    def remove(*addresses)
      if addresses.empty?
        say('No IP(s) given', :red)
        exit(1)
      end

      processed = 0
      skipped   = 0

      addresses.each do |address|
        ip_blocks = if options[:force]
                      IpBlock.where('ip >>= ?', address)
                    else
                      IpBlock.where('ip <<= ?', address)
                    end

        if ip_blocks.empty?
          say("#{address} is not yet blocked", :yellow)
          skipped += 1
          next
        end

        ip_blocks.in_batches.destroy_all
        processed += 1
      end

      say("Removed #{processed}, skipped #{skipped}", color(processed, 0))
    end

    option :format, aliases: [:f], enum: %w(plain nginx), desc: 'Format of the output'
    desc 'export', 'Export blocked IPs'
    long_desc <<-LONG_DESC
      Export blocked IPs. Different formats are supported for usage with other
      tools. Only blocks with no_access severity are returned.
    LONG_DESC
    def export
      IpBlock.where(severity: :no_access).find_each do |ip_block|
        case options[:format]
        when 'nginx'
          puts "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
        else
          puts "#{ip_block.ip}/#{ip_block.ip.prefix}"
        end
      end
    end

    private

    def color(processed, failed)
      if !processed.zero? && failed.zero?
        :green
      elsif failed.zero?
        :yellow
      else
        :red
      end
    end
  end
end