aboutsummaryrefslogtreecommitdiffstats
path: root/src/sap/client.rb
blob: 9eaed0a3496e48c9e2a0e7354cb55d1010afe75e (plain)
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# encoding: UTF-8
=begin
This file is part of softSIM.

softSIM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

softSIM is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with sofSIM.  If not, see <http://www.gnu.org/licenses/>.

Copyright (C) 2011 Kevin "tsaitgaist" Redon kevredon@mail.tsaitgaist.info
=end
# this is the client part of the SAP
# it implements the state machine for the client
require_relative 'common'

# this is an abstract class
# TODO :
# - verify state before sending
# - respect max size (and require min size)
# - ERROR_RESP handling
class Client < SAP

  # make the class abstract
  private :initialize
  
  def initialize(io)
    super(io)

    # state of the state machine
    @state = nil

    # initiate the state machine (connect_req)
    set_state :not_connected
    @max_msg_size = 0xffff

    # sim can be used
    @sim_ok = false
  end

  def start
    # start the client in another thread
    thread = Thread.new do
      super
    end
    thread.abort_on_exception = true
  end

  # add sim access protection
  def set_state (state)
    super(state)
    if @state==:not_connected or @state==:connection_under_negociation then
      @sim_ok = false
    end
  end

  # verify result code
  def result_ok?(message)
    # is it a ResultCode
    raise "message does not contains a result code" unless message[:payload][0][:id]==0x02
    if message[:payload][0][:value][0]==0x00 then
      return true
    else
      log("error","message result code : #{RESULT_CODE[message[:payload][0][:value][0]]}",1)
      return false
    end
  end

  def state_machine(message)
    # check direction
    raise "got client to server message" unless message[:server_to_client]
    case message[:name]
    when "CONNECT_RESP"
      raise "msg #{message[:name]} in wrong state : #{@state}" unless @state==:connection_under_negociation
      connection_status = message[:payload][0][:value][0]
      max_msg_size = nil
      # print response
      if message[:payload].size == 1 then
        log("client","connection : #{CONNECTION_STATUS[connection_status]}",3)
      elsif message[:payload].size == 2 then
        max_msg_size = (message[:payload][1][:value][0]<<8)+message[:payload][1][:value][1]
        log("client","connection : #{CONNECTION_STATUS[connection_status]} (max message size = #{max_msg_size})",3)
      end
      # verify response
      if connection_status==0x00 then
        # OK, Server can fulfill requirements
        log("client","connected to server",3)
        set_state :idle
      elsif connection_status==0x02 and message[:payload].size==2 then
        # Error, Server does not support maximum message size
        log("client","server can not handle size. adapting",3)
        @max_msg_size = max_msg_size
        set_state :not_connected
      else
        set_state :not_connected
        raise "connection error"
      end
    when "DISCONNECT_RESP"
      log("client","disconnected",3)
      set_state :not_connected
      @end=true
    when "STATUS_IND"
      status = message[:payload][0][:value][0]
      log("client","new card status : #{STATUS_CHANGE[status]}",3)
      if status==0x01 then
        # card reset
        @sim_ok = true
      else
        @sim_ok = false
      end
    when "TRANSFER_ATR_RESP"
      raise "msg #{message[:name]} in wrong state : #{@state}" unless @state==:processing_atr_request
      if result_ok?(message) then
        @atr = message[:payload][1][:value]
      else
        #TODO : raise error, or retry later ?
      end
      set_state :idle
    when "TRANSFER_APDU_RESP"
      raise "msg #{message[:name]} in wrong state : #{@state}" unless @state==:processing_apdu_request
      if result_ok?(message) then
        @apdu = message[:payload][1][:value]
      else
        #TODO : raise error, or retry later ?
      end
      set_state :idle
    when "ERROR_RESP"
      log("error","got an error response",1)
      if @state==:connection_under_negociation then
        set_state :not_connected
      elsif @state!=:not_connected and @state!=:idle then
        set_state :idle
      end
    else
      raise "not implemented or unknown message type : #{message[:name]}"
    end
  end

  def connect
    log("client","connecting",3)
    # wait to be connected
    until @state==:idle do
      if @state == :not_connected then
        payload = []
        # ["MaxMsgSize",[size]]
        payload << [0x00,[(@max_msg_size>>8)&0xff,@max_msg_size&0xff]]
        connect = create_message("CONNECT_REQ",payload)
        send(connect)
        set_state :connection_under_negociation
      elsif @state!=:connection_under_negociation and @state!=:idle
        raise "can not connect. required state : not_connected, current state : #{@state}"
        return false
      end
    end
    # wait for the sim to be ready
    until @sim_ok do
      sleep @wait_time
    end
    return true
  end

  def disconnect
    log("client","disconnecting",3)
    if @state==:not_connected or @state==:connection_under_negociation then
      raise "can not disconnect. must be connected, current state : #{@state}"
      return false
    else # send DISCONNECT_REQ
      connect = create_message("DISCONNECT_REQ")
      send(connect)
      until @state==:not_connected
        sleep @wait_time
      end
      return true
    end
  end
  
  # return ATR (byte array)
  def atr
    if @state==:idle then
      connect = create_message("TRANSFER_ATR_REQ")
      send(connect)
      set_state :processing_atr_request
      # wait for the ATR
      until @state==:idle
        sleep @wait_time
      end
      log("ATR","#{hex(@atr)}",1)
      return @atr
    else
      raise "can not ask ATR. must be  in state idle, current state : #{@state}"
      return nil
    end
  end

  # return the response of the apdu request
  def apdu(request)
    raise "APDU request empty" unless request and request.size>=5
    log("APDU","< #{hex(request)}",1)
    if @state==:idle then
      # ["CommandAPDU",[apdu]]
      connect = create_message("TRANSFER_APDU_REQ",[[0x04,request]])
      send(connect)
      set_state :processing_apdu_request
      # wait for the ATR
      until @state==:idle
        sleep @wait_time
      end
      log("APDU","> #{hex(@apdu)}",1)
      return @apdu
    else
      raise "can not sen APDU request. must be  in state idle, current state : #{@state}"
      return nil
    end
  end
end