Skip to content

Commit

Permalink
refactored Login class for new login procedure
Browse files Browse the repository at this point in the history
  • Loading branch information
luk4s committed Jan 21, 2025
1 parent 166e770 commit 84af382
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 217 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
ruby-version: 3.3.6
bundler-cache: true
- uses: browser-actions/setup-firefox@latest
- name: Run tests
run: bundle exec rspec
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@

# rspec failure tracking
.rspec_status
.env
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require:
AllCops:
NewCops: enable
SuggestExtensions: false
TargetRubyVersion: 3.2
TargetRubyVersion: 3.3

Style/StringLiterals:
Enabled: true
Expand Down
10 changes: 0 additions & 10 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ PATH
i18n (~> 1.14)
nokogiri (~> 1.15)
rest-client (~> 2.1)
selenium-webdriver (~> 4.25.0)

GEM
remote: https://rubygems.org/
specs:
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0)
bigdecimal (3.1.9)
coderay (1.1.3)
concurrent-ruby (1.3.5)
Expand Down Expand Up @@ -90,13 +88,6 @@ GEM
rubocop-rspec (3.4.0)
rubocop (~> 1.61)
ruby-progressbar (1.13.0)
rubyzip (2.4.1)
selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
Expand All @@ -110,7 +101,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
websocket (1.2.11)

PLATFORMS
arm64-darwin-22
Expand Down
3 changes: 1 addition & 2 deletions atrea_control.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
spec.summary = "Get data control.atrea.eu"
spec.description = "Read data from web controller of RD5 duplex by Atrea."
spec.homepage = "https://github.com/luk4s/atrea_control"
spec.required_ruby_version = Gem::Requirement.new("~> 3.2")
spec.required_ruby_version = Gem::Requirement.new("~> 3.3")

spec.metadata["allowed_push_host"] = "https://rubygems.org"

Expand Down Expand Up @@ -39,5 +39,4 @@ Gem::Specification.new do |spec|
spec.add_dependency "i18n", "~> 1.14"
spec.add_dependency "nokogiri", "~> 1.15"
spec.add_dependency "rest-client", "~> 2.1"
spec.add_dependency "selenium-webdriver", "~> 4.25.0"
end
2 changes: 1 addition & 1 deletion lib/atrea_control/duplex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module AtreaControl
# Controller for +control.atrea.eu+
module Duplex
CONTROL_URI = "https://control.atrea.eu/"
CONTROL_VERSION = "003001009"
CONTROL_VERSION = "3001022"

autoload :Login, "atrea_control/duplex/login"
autoload :Request, "atrea_control/duplex/request"
Expand Down
176 changes: 89 additions & 87 deletions lib/atrea_control/duplex/login.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
# frozen_string_literal: true

require "digest"
require "nokogiri"
require "rest-client"
require "selenium-webdriver"
require "securerandom"

module AtreaControl
module Duplex
# Process login into RD5 with selenium to get `sid` ( auth_token ) for direct API communication
# Process login into RD5 to get `sid` ( auth_token ) for direct API communication
class Login
include AtreaControl::Logger

# @return [Hash] - user_id, unit_id, sid
def self.user_tokens(login:, password:)
i = new(login: login, password: password)
tokens = i.user

tokens
ensure
i.close
instance = new(login: login, password: password)
instance.call
end

# @param [String] login
Expand All @@ -26,101 +22,107 @@ def initialize(login:, password:)
@login = login
@password = password
end
#
# def crypto_password
# md5 = Digest::MD5.new
# md5 << "\r\n"
# md5 << @password
# md5.hexdigest
# end
#
# def token
# RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/config/login.cgi", params: { magic: crypto_password }
# end

def user
raise AtreaControl::Error, "Must be logged in" unless login
# Perform login procedure for retrieve `sid` (auth_token)
# @return [Hash] - user_id, unit_id, sid
# @raise [AtreaControl::Error] if login failed
def call
@sid = sid
if @sid == "0"
re_login = RestClient.post "#{AtreaControl::Duplex::CONTROL_URI}/apps/rd5Control/handle.php?action=unitLogin&user=#{user_id}&unit=#{unit_id}&table=userUnits&idPwd=#{unit[:iid]}&#{SecureRandom.hex(2)}&_ts=#{SecureRandom.hex(4)}",
{ comm: "config/login.cgi?magic=" }, headers
time = Nokogiri::XML(re_login.body).at_xpath("//sended")["time"].to_i
logger.debug "Login in #{time} seconds..."
time.times do
@sid = sid
break if @sid != "0"

sleep 1
end
raise AtreaControl::Error, "Login failed" if @sid == "0"

logger.debug "Login complete !"
else
logger.debug "Login is not necessary ! SID: #{@sid}"
end
{ user_id:, unit_id:, sid: @sid }
end

logger.debug "refresh user data based on session"
@user_id = driver.execute_script("return window._user")
@unit_id = driver.execute_script("return window._unit")
@auth_token = driver.execute_script("return window.user")&.[]("auth") # sid
private

{ user_id: @user_id, unit_id: @unit_id, sid: @auth_token }
end
# @!group Login steps, order is important

# @return [Selenium::WebDriver::Firefox::Driver]
def driver
return @driver if defined?(@driver)
# Retrieve user details from RD5 core.php?action=init
# @return [Hash] - user_id, name
def user
core_init = RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/core/core.php?action=init&_ts=#{SecureRandom.hex(4)}",
headers
client = Nokogiri::XML(core_init.body).at_xpath("//client")
user_id = client["id"]
name = client["name"]
logger.debug "User ID: #{user_id}, User Name: #{name}"

{ user_id:, name: }
end

# options = Selenium::WebDriver::Firefox::Options.new
# options.headless! unless ENV["NO_HEADLESS"]
# @driver ||= Selenium::WebDriver.for :firefox, capabilities: [options]
options = Selenium::WebDriver::Firefox::Options.new
options.add_argument "-headless" unless ENV["NO_HEADLESS"]
@driver ||= Selenium::WebDriver::Firefox::Driver.new options: options
def user_id
@user_id ||= user[:user_id]
end

# Login into control
def login
return driver if @logged

@login_in_progress = true
logger.debug "start new login..."
driver.get "#{AtreaControl::Duplex::CONTROL_URI}?action=logout"
submit_login_form
finish_login
driver
ensure
@login_in_progress = false
# For some reason, this requests must be done before `unit_id` requested
def run_rd5_app
RestClient.post "#{AtreaControl::Duplex::CONTROL_URI}/core/core.php?Sync=1&action=run&object=app&lng=28&rVer=1&_ts=#{SecureRandom.hex(4)}",
{ name: "rd5Control", path: "apps/rd5Control/" }, headers
RestClient.post "#{AtreaControl::Duplex::CONTROL_URI}/core/core.php?Sync=1&action=load&object=setting&_ts=#{SecureRandom.hex(4)}",
{ path: "apps/rd5Control" }, headers
end

# Submit given credentials and proceed login
def submit_login_form
form = driver.find_element(id: "loginFrm")
username = form.find_element(name: "username")
username.send_keys @login
password = form.find_element(name: "password")
password.send_keys @password
logger.debug "Submit login form..."

submit = form.find_element(css: "input[type=submit]")
submit.click
# Retrieve overview of RD5 unit
# @return [Hash] - unit_number (digit code/ID from list) and iid (unit salt?)
def unit
return @unit if @unit

# run_rd5_app
units_table = RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/_data/data.php?Sync=1&action=getdata&rH&rE&table=userUnits&ds=rd5&_ts=#{SecureRandom.hex(4)}",
headers
item = Nokogiri::XML(units_table.body).at_xpath("//i")
unit_number = item["unit"]
iid = item["id"]
@unit ||= { unit_number:, iid: }
end

# Retrieve dashboard URI from object tag and open it again
def open_dashboard
uri = driver.find_element(tag_name: "object").attribute "data"
# Open "iframe" with atrea dashboard - it propagate window objects...
driver.get uri
logger.debug "login success"
@logged = true
# With `unit_number` from `unit` method, get `unit_id` from RD5 unit records
# @return [String] - unit_id
def unit_id
return @unit_id if @unit_id

records = RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/_data/data.php?Sync=1&action=getrecord&id=#{unit[:unit_number]}&table=units&ds=rd5&_ts=#{SecureRandom.hex(4)}",
headers
@unit_id ||= Nokogiri::XML(records.body).at_xpath("//table/i")["ident"]
end

# quit selenium browser
def close
begin
driver.quit
rescue StandardError
nil
end
logger.debug "driver closed & destroyed"
ensure
remove_instance_variable :@driver
def sid
data = RestClient.get "#{AtreaControl::Duplex::CONTROL_URI}/apps/rd5Control/handle.php?Sync=1&action=unitQuery&query=loged&user=#{user_id}&unit=#{unit_id}&#{SecureRandom.hex(2)}&_ts=#{SecureRandom.hex(4)}",
headers
logger.debug data.body
Nokogiri::XML(data.body).at_xpath("//login")["sid"]
end

private
# @!group Private methods

def finish_login
30.times do |i|
return true if open_dashboard
rescue Selenium::WebDriver::Error::NoSuchElementError => e
logger.debug e.message
logger.debug "#{i + 1}/30 attempt for login..."
sleep 10
# @return [String] session ID from PHP BE
def php_session_id
return @php_session_id if @php_session_id

payload = { username: @login, password: @password }
RestClient.post "#{AtreaControl::Duplex::CONTROL_URI}?action=login", payload do |response|
@php_session_id = response.cookies["PHPSESSID"]
end
File.write("/tmp/failed_login-#{@login}.html", driver.page_source)
raise AtreaControl::Error, "unable to login"
@php_session_id
end

def headers
{ cookies: { PHPSESSID: php_session_id }, "App-name": "rd5Control" }
end
end
end
Expand Down
67 changes: 0 additions & 67 deletions lib/rest-client-only.rb

This file was deleted.

Loading

0 comments on commit 84af382

Please sign in to comment.