Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: Scan files when a single server is online #42

Merged
merged 6 commits into from
Aug 18, 2023
Merged

Conversation

nbianca
Copy link
Member

@nbianca nbianca commented Aug 2, 2023

This commits makes sure that even when a single server is online, the plugin is available. This commit also makes sure that a random and online server is chosen when using with_session without any specified socket. It used to use the first one.

This commits makes sure that even when a single server is online,
the plugin is available. This commit also makes sure that a random
and online server is chosen when using with_session without any
specified socket. It used to use the first one.
Copy link
Member

@ZogStriP ZogStriP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chances this could be tested? 🤔

Copy link
Member

@romanrizzi romanrizzi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is more or less what I had in mind but we need some more changes. Right now, #with_session closes the socket, so the available socket you find cannot be used to scan a file.

lib/discourse_antivirus/clamav.rb Outdated Show resolved Hide resolved
@romanrizzi
Copy link
Member

We should change #target_online? to do a PING without opening a session. This way, the socket don't get closed. I checked ClamAV's docs and it doesn't look necessary.

Besides any test we could write for this, we need to try it locally just to be sure it will work as we expect. We completely mock ClamAV on tests.

@nbianca
Copy link
Member Author

nbianca commented Aug 2, 2023

Any chances this could be tested? 🤔

I do not think testing would help because we would have to mock all the socket-related logic and thus lose a lot of the benefits of testing. In other words, the tests would just check that the mocks behave as we want.

PING without opening a session

This is interesting, you mean pinging without a session and then reusing the same socket to do the scan, right?

I will also try to run ClamAV in Docker and test it with that.

@romanrizzi
Copy link
Member

This is interesting, you mean pinging without a session and then reusing the same socket to do the scan, right?

Yes, doing the PING without going through #with_session to avoid closing the socket before using it.

I believe we'll also need a timeout In our IO#select calls, and assume it's not available if it returns nil. I was too optimistic when I wrote this code 😅

@nbianca
Copy link
Member Author

nbianca commented Aug 2, 2023

I am not sure it works using the same socket for PING and some other commands. I tried it like this, but the last response looks empty.

pry(main)> socket = TCPSocket.new("127.0.0.1", 3310, connect_timeout: 3)
=> #<TCPSocket:fd 20, AF_INET, 127.0.0.1, 58433>
pry(main)> socket.sendmsg_nonblock("zPING\0", 0, nil)
=> 6
pry(main)> socket.recvmsg_nonblock(25)
=> ["PONG\x00", #<Addrinfo: empty-sockaddr SOCK_STREAM>, 128]
pry(main)> socket.sendmsg_nonblock("zPING\0", 0, nil)
=> 6
pry(main)> socket.recvmsg_nonblock(25)
=> ["", #<Addrinfo: empty-sockaddr SOCK_STREAM>, 128]

@nbianca
Copy link
Member Author

nbianca commented Aug 3, 2023

@romanrizzi, I looked into this further and I have found out that only one command can be sent on a socket and then we must close it and reopen it unless we use the sessions. This is strange because it means we must put the PING inside the session and make sure we don't close the session if we want to scan a file.

@romanrizzi
Copy link
Member

We'll have to organize things differently to iterate over different connections and use it only if ClamAV is online. Here's how I imagine this should work:

  1. We expose a way to lazily iterate over all sockets, returning the first ClamAV response we get:
class ClamAVServicesPool
  def on_online_target
    response = nil

    service_instance.targets.each do |target|
      socket = build_socket(target)
      next if socket.nil?

      response = yield(socket)
      break if response
    end

    response
  end
end
  1. We use it and pass it a block that does what we want. Here's an example of how we check if the target is online:
class ClamAV
  def ping(socket)
    write_in_socket(socket, "zPING\0")
  end

  def accepting_connections?
    response = clamav_services_pool.on_online_target do |socket|
      with_session(socket) { ping(socket) }
    end

    available = response.present? && clean_msg(response) == "PONG"
    update_status(!available)
    available
  end
end

Of course, this is the easiest part but the spirit of the example is that we separate responsabilities: ClamAVServicePool is in charge of opening connections against a server, which means the machine is up. On the other hand, ClamAV takes care of checking the daemon is running inside that machine to update status, stats, or scan files. It checks if it can write on the socket, and if it respond to a ping, returning early if it doesn't, which means we repeat the process on the next socket.

@nbianca
Copy link
Member Author

nbianca commented Aug 4, 2023

I think I have a better suggestion and that is to refactor ClamAVServicesPool to be what the name says: a pool of services, instead of a pool of sockets. Then, the service instance can be used to connect as many times as needed and check if the server replies to PING requests.

However, I did not get yet to fix the specs, but I manually tested it and it works well.

@romanrizzi
Copy link
Member

@nbianca - I reorganized the code a bit following your suggestion! Although they are a multiple entities playing different roles, I think this makes the code easier to follow.

Please feel free to revert or make any changes you want if you think there's a better solution.

Copy link
Member Author

@nbianca nbianca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@romanrizzi, I like the changes. Do you think we can merge it?

lib/discourse_antivirus/clamav_services_pool.rb Outdated Show resolved Hide resolved
lib/discourse_antivirus/clamav.rb Outdated Show resolved Hide resolved
This change builds upon Bianca's idea of create the `ClamAVService` class and reorganized the code based on the following rationale:

`ClamAV` is what glues everything together. It knows how to talk to the S3 store, the plugin store and the `ClamAVService` to display the version, check availability, and scan files. It gets the raw response from ClamAV and translates to a different format which the rest of the plugin relies on.
`ClamAVService` defines an interface of which operations are possible (`online?`, `scan_file`, `version`) and takes care of the socket and session managemente, as well as writting/reading from the socket.
`ClamAVServicePool` resolves the SRV record and instantiates existing services. It tells them how to open a connection.

While working on this, I tried to remove the latter and do everything using only the first two, but I decided to keep it in the end because this design is handy for testing. We rely on the `FakeTCPSocket` for spying on the communication and returning specific responses, and this design let us use without relying on a mocking framework, just dependency injection.
@romanrizzi romanrizzi merged commit c4f3e33 into main Aug 18, 2023
3 checks passed
@romanrizzi romanrizzi deleted the fix_single branch August 18, 2023 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
3 participants