aspera-cli 4.17.0 → 4.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -4
  3. data/CHANGELOG.md +23 -0
  4. data/CONTRIBUTING.md +15 -1
  5. data/README.md +620 -378
  6. data/bin/ascli +5 -0
  7. data/bin/asession +2 -2
  8. data/lib/aspera/agent/alpha.rb +6 -4
  9. data/lib/aspera/agent/base.rb +9 -6
  10. data/lib/aspera/agent/connect.rb +4 -4
  11. data/lib/aspera/agent/direct.rb +56 -37
  12. data/lib/aspera/agent/httpgw.rb +23 -324
  13. data/lib/aspera/agent/node.rb +19 -20
  14. data/lib/aspera/agent/trsdk.rb +19 -20
  15. data/lib/aspera/api/aoc.rb +17 -14
  16. data/lib/aspera/api/cos_node.rb +4 -4
  17. data/lib/aspera/api/httpgw.rb +339 -0
  18. data/lib/aspera/api/node.rb +34 -21
  19. data/lib/aspera/ascmd.rb +4 -3
  20. data/lib/aspera/ascp/installation.rb +15 -7
  21. data/lib/aspera/ascp/management.rb +2 -2
  22. data/lib/aspera/cli/basic_auth_plugin.rb +5 -9
  23. data/lib/aspera/cli/extended_value.rb +12 -6
  24. data/lib/aspera/cli/formatter.rb +155 -65
  25. data/lib/aspera/cli/hints.rb +18 -0
  26. data/lib/aspera/cli/main.rb +22 -29
  27. data/lib/aspera/cli/manager.rb +53 -36
  28. data/lib/aspera/cli/plugin.rb +26 -17
  29. data/lib/aspera/cli/plugin_factory.rb +31 -20
  30. data/lib/aspera/cli/plugins/alee.rb +14 -2
  31. data/lib/aspera/cli/plugins/aoc.rb +141 -131
  32. data/lib/aspera/cli/plugins/ats.rb +1 -1
  33. data/lib/aspera/cli/plugins/config.rb +52 -46
  34. data/lib/aspera/cli/plugins/console.rb +8 -5
  35. data/lib/aspera/cli/plugins/faspex.rb +27 -19
  36. data/lib/aspera/cli/plugins/faspex5.rb +222 -149
  37. data/lib/aspera/cli/plugins/faspio.rb +85 -0
  38. data/lib/aspera/cli/plugins/httpgw.rb +55 -0
  39. data/lib/aspera/cli/plugins/node.rb +86 -29
  40. data/lib/aspera/cli/plugins/orchestrator.rb +31 -29
  41. data/lib/aspera/cli/plugins/preview.rb +6 -2
  42. data/lib/aspera/cli/plugins/server.rb +5 -5
  43. data/lib/aspera/cli/plugins/shares.rb +16 -14
  44. data/lib/aspera/cli/sync_actions.rb +6 -6
  45. data/lib/aspera/cli/transfer_agent.rb +5 -4
  46. data/lib/aspera/cli/version.rb +1 -1
  47. data/lib/aspera/environment.rb +7 -6
  48. data/lib/aspera/faspex_gw.rb +5 -4
  49. data/lib/aspera/faspex_postproc.rb +2 -2
  50. data/lib/aspera/log.rb +6 -3
  51. data/lib/aspera/node_simulator.rb +2 -2
  52. data/lib/aspera/oauth/base.rb +31 -19
  53. data/lib/aspera/oauth/factory.rb +12 -13
  54. data/lib/aspera/oauth/generic.rb +1 -0
  55. data/lib/aspera/oauth/jwt.rb +18 -15
  56. data/lib/aspera/oauth/url_json.rb +8 -6
  57. data/lib/aspera/open_application.rb +5 -7
  58. data/lib/aspera/persistency_folder.rb +2 -2
  59. data/lib/aspera/preview/generator.rb +3 -3
  60. data/lib/aspera/preview/options.rb +3 -3
  61. data/lib/aspera/preview/terminal.rb +4 -4
  62. data/lib/aspera/preview/utils.rb +3 -3
  63. data/lib/aspera/proxy_auto_config.rb +5 -1
  64. data/lib/aspera/rest.rb +60 -74
  65. data/lib/aspera/rest_call_error.rb +1 -1
  66. data/lib/aspera/rest_error_analyzer.rb +2 -2
  67. data/lib/aspera/rest_errors_aspera.rb +1 -1
  68. data/lib/aspera/resumer.rb +1 -1
  69. data/lib/aspera/secret_hider.rb +2 -4
  70. data/lib/aspera/ssh.rb +1 -1
  71. data/lib/aspera/transfer/parameters.rb +39 -36
  72. data/lib/aspera/transfer/spec.rb +2 -0
  73. data/lib/aspera/transfer/sync.rb +2 -1
  74. data/lib/aspera/transfer/uri.rb +1 -1
  75. data/lib/aspera/uri_reader.rb +5 -4
  76. data/lib/aspera/web_auth.rb +1 -1
  77. data/lib/aspera/web_server_simple.rb +4 -3
  78. data.tar.gz.sig +0 -0
  79. metadata +5 -3
  80. metadata.gz.sig +0 -0
  81. data/lib/aspera/cli/plugins/bss.rb +0 -71
@@ -50,8 +50,9 @@ module Aspera
50
50
  operation: 'POST',
51
51
  subpath: "packages/#{package['id']}/transfer_spec/upload",
52
52
  headers: {'Accept' => 'application/json'},
53
- url_params: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
54
- json_params: {paths: [{'destination'=>'/'}]}
53
+ query: {transfer_type: Cli::Plugins::Faspex5::TRANSFER_CONNECT},
54
+ body: {paths: [{'destination'=>'/'}]},
55
+ body_type: :json
55
56
  )[:data]
56
57
  transfer_spec.delete('authentication')
57
58
  # but we place it in a Faspex package creation response
@@ -94,5 +95,5 @@ module Aspera
94
95
  response.body = {error: 'Unsupported endpoint'}.to_json
95
96
  end
96
97
  end
97
- end # Faspex4GWServlet
98
- end # Aspera
98
+ end
99
+ end
@@ -74,5 +74,5 @@ module Aspera
74
74
  response.body = {status: 'error', script: script_path, message: e.message}.to_json
75
75
  end
76
76
  end
77
- end # Faspex4PostProcServlet
78
- end # Aspera
77
+ end
78
+ end
data/lib/aspera/log.rb CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  require 'aspera/colors'
4
4
  require 'aspera/secret_hider'
5
- require 'aspera/environment'
6
- require 'aspera/assert'
7
5
  require 'logger'
8
6
  require 'pp'
9
7
  require 'json'
10
8
  require 'singleton'
11
9
 
10
+ old_verbose = $VERBOSE
11
+ $VERBOSE = nil
12
+
12
13
  # extend Ruby logger with trace levels
13
14
  class Logger
14
15
  TRACE_MAX = 2
@@ -44,6 +45,8 @@ class Logger
44
45
  Logger::Severity.constants.each { |severity| make_methods(severity) }
45
46
  end
46
47
 
48
+ $VERBOSE = old_verbose
49
+
47
50
  module Aspera
48
51
  # Singleton object for logging
49
52
  class Log
@@ -84,7 +87,7 @@ module Aspera
84
87
  ensure
85
88
  $stderr = real_stderr
86
89
  end
87
- end # class
90
+ end
88
91
 
89
92
  attr_reader :logger_type, :logger
90
93
  attr_writer :program_name
@@ -209,5 +209,5 @@ module Aspera
209
209
  session_id: 'bafc72b8-366c-4501-8095-47208183d6b8'}]
210
210
  }
211
211
  end
212
- end # NodeSimulatorServlet
213
- end # Aspera
212
+ end
213
+ end
@@ -9,9 +9,7 @@ require 'date'
9
9
  module Aspera
10
10
  module OAuth
11
11
  # Implement OAuth 2 for the REST client and generate a bearer token
12
- # call get_authorization() to get a token.
13
- # bearer tokens are kept in memory and also in a file cache for later re-use
14
- # if a token is expired (api returns 4xx), call again get_authorization(refresh: true)
12
+ # bearer tokens are cached in memory and in a file cache for later re-use
15
13
  # https://tools.ietf.org/html/rfc6749
16
14
  class Base
17
15
  # scope can be modified after creation
@@ -31,6 +29,7 @@ module Aspera
31
29
  client_id: nil,
32
30
  client_secret: nil,
33
31
  scope: nil,
32
+ use_query: false,
34
33
  path_token: 'token', # default endpoint for /token to generate token
35
34
  token_field: 'access_token' # field with token in result of call to path_token
36
35
  )
@@ -42,6 +41,7 @@ module Aspera
42
41
  @client_id = client_id
43
42
  @client_secret = client_secret
44
43
  @scope = scope
44
+ @use_query = use_query
45
45
  @identifiers = []
46
46
  @identifiers.push(auth[:username]) if auth.is_a?(Hash) && auth.key?(:username)
47
47
  # this is the OAuth API
@@ -52,13 +52,22 @@ module Aspera
52
52
  end
53
53
 
54
54
  # helper method to create token as per RFC
55
- def create_token_call(www_params)
55
+ def create_token_call(creation_params)
56
56
  Log.log.debug{'Generating a new token'.bg_green}
57
+ payload = {
58
+ body: creation_params,
59
+ body_type: :www
60
+ }
61
+ if @use_query
62
+ payload[:query] = creation_params
63
+ payload[:body] = {}
64
+ end
57
65
  return @api.call(
58
- operation: 'POST',
59
- subpath: @path_token,
60
- headers: {'Accept' => 'application/json'},
61
- www_body_params: www_params)
66
+ operation: 'POST',
67
+ subpath: @path_token,
68
+ headers: {'Accept' => 'application/json'},
69
+ **payload
70
+ )
62
71
  end
63
72
 
64
73
  # @return Hash with optional general parameters
@@ -70,24 +79,27 @@ module Aspera
70
79
  return call_params
71
80
  end
72
81
 
73
- # OAuth v2 token generation
74
- # @param use_refresh_token set to true to force refresh or re-generation (if previous failed)
75
- def get_authorization(use_refresh_token: false, use_cache: true)
82
+ # get an OAuth v2 token (generated, cached, refreshed)
83
+ # call token() to get a token.
84
+ # if a token is expired (api returns 4xx), call again token(refresh: true)
85
+ # @param cache set to false to disable cache
86
+ # @param refresh set to true to force refresh or re-generation (if previous failed)
87
+ def token(cache: true, refresh: false)
76
88
  # generate token unique identifier for persistency (memory/disk cache)
77
89
  token_id = IdGenerator.from_list(Factory.id(
78
90
  @base_url,
79
- @grant_method,
91
+ Factory.class_to_id(self.class),
80
92
  @identifiers,
81
93
  @scope
82
94
  ))
83
95
 
84
96
  # get token_data from cache (or nil), token_data is what is returned by /token
85
- token_data = Factory.instance.persist_mgr.get(token_id) if use_cache
97
+ token_data = Factory.instance.persist_mgr.get(token_id) if cache
86
98
  token_data = JSON.parse(token_data) unless token_data.nil?
87
99
  # Optional optimization: check if node token is expired based on decoded content then force refresh if close enough
88
100
  # might help in case the transfer agent cannot refresh himself
89
101
  # `direct` agent is equipped with refresh code
90
- if !use_refresh_token && !token_data.nil?
102
+ if !refresh && !token_data.nil?
91
103
  decoded_token = OAuth::Factory.instance.decode_token(token_data[@token_field])
92
104
  Log.log.debug{Log.dump('decoded_token', decoded_token)} unless decoded_token.nil?
93
105
  if decoded_token.is_a?(Hash)
@@ -96,13 +108,13 @@ module Aspera
96
108
  elsif decoded_token['exp'].is_a?(Integer) then Time.at(decoded_token['exp'])
97
109
  end
98
110
  # force refresh if we see a token too close from expiration
99
- use_refresh_token = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.globals[:token_expiration_guard_sec]
100
- Log.log.debug{"Expiration: #{expires_at_sec} / #{use_refresh_token}"}
111
+ refresh = true if expires_at_sec.is_a?(Time) && (expires_at_sec - Time.now) < OAuth::Factory.instance.parameters[:token_expiration_guard_sec]
112
+ Log.log.debug{"Expiration: #{expires_at_sec} / #{refresh}"}
101
113
  end
102
114
  end
103
115
 
104
116
  # an API was already called, but failed, we need to regenerate or refresh
105
- if use_refresh_token
117
+ if refresh
106
118
  if token_data.is_a?(Hash) && token_data.key?('refresh_token') && !token_data['refresh_token'].eql?('not_supported')
107
119
  # save possible refresh token, before deleting the cache
108
120
  refresh_token = token_data['refresh_token']
@@ -133,11 +145,11 @@ module Aspera
133
145
  json_data = resp[:http].body
134
146
  token_data = JSON.parse(json_data)
135
147
  Factory.instance.persist_mgr.put(token_id, json_data)
136
- end # if ! in_cache
148
+ end
137
149
  Aspera.assert(token_data.key?(@token_field)){"API error: No such field in answer: #{@token_field}"}
138
150
  # ok we shall have a token here
139
151
  return OAuth::Factory.bearer_build(token_data[@token_field])
140
152
  end
141
- end # OAuth
153
+ end
142
154
  end
143
155
  end
@@ -5,6 +5,7 @@ require 'aspera/assert'
5
5
  require 'base64'
6
6
  module Aspera
7
7
  module OAuth
8
+ # Factory to create tokens and manage their cache
8
9
  class Factory
9
10
  include Singleton
10
11
  # a prefix for persistency of tokens (simplify garbage collect)
@@ -16,7 +17,11 @@ module Aspera
16
17
 
17
18
  class << self
18
19
  def bearer_build(token)
19
- return BEARER_PREFIX + token
20
+ return "#{BEARER_PREFIX}#{token}"
21
+ end
22
+
23
+ def bearer?(token)
24
+ return token.start_with?(BEARER_PREFIX)
20
25
  end
21
26
 
22
27
  def bearer_extract(token)
@@ -24,14 +29,11 @@ module Aspera
24
29
  return token[BEARER_PREFIX.length..-1]
25
30
  end
26
31
 
27
- def bearer?(token)
28
- return token.start_with?(BEARER_PREFIX)
29
- end
30
-
31
32
  def id(*params)
32
33
  return [PERSIST_CATEGORY_TOKEN, *params].flatten
33
34
  end
34
35
 
36
+ # snake version of class name is the identifier
35
37
  def class_to_id(creator_class)
36
38
  return creator_class.name.split('::').last.capital_to_snake.to_sym
37
39
  end
@@ -45,11 +47,8 @@ module Aspera
45
47
  # token creation methods
46
48
  @token_type_classes = {}
47
49
  @decoders = []
48
- @globals = {
49
- # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
50
- jwt_accepted_offset_sec: 300,
51
- # one hour validity (TODO: configurable?)
52
- jwt_expiry_offset_sec: 3600,
50
+ # default parameters, others can be added by handlers
51
+ @parameters = {
53
52
  # tokens older than 30 minutes will be discarded from cache
54
53
  token_cache_expiry_sec: 1800,
55
54
  # tokens valid for less than this duration will be regenerated
@@ -59,12 +58,12 @@ module Aspera
59
58
 
60
59
  public
61
60
 
62
- attr_reader :globals
61
+ attr_reader :parameters
63
62
 
64
63
  def persist_mgr=(manager)
65
64
  @persist = manager
66
65
  # cleanup expired tokens
67
- @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @globals[:token_cache_expiry_sec])
66
+ @persist.garbage_collect(PERSIST_CATEGORY_TOKEN, @parameters[:token_cache_expiry_sec])
68
67
  end
69
68
 
70
69
  def persist_mgr
@@ -104,7 +103,7 @@ module Aspera
104
103
  # @param id_create called to generate unique id for token, for cache
105
104
  def register_token_creator(creator_class)
106
105
  Aspera.assert_type(creator_class, Class)
107
- id = self.class.class_to_id(creator_class)
106
+ id = Factory.class_to_id(creator_class)
108
107
  Log.log.debug{"registering token creator #{id}"}
109
108
  @token_type_classes[id] = creator_class
110
109
  end
@@ -4,6 +4,7 @@ require 'aspera/oauth/base'
4
4
 
5
5
  module Aspera
6
6
  module OAuth
7
+ # Generic token creator
7
8
  class Generic < Base
8
9
  def initialize(
9
10
  grant_type:,
@@ -5,41 +5,44 @@ require 'aspera/assert'
5
5
  require 'securerandom'
6
6
  module Aspera
7
7
  module OAuth
8
+ # remove 5 minutes to account for time offset between client and server (TODO: configurable?)
9
+ Factory.instance.parameters[:jwt_accepted_offset_sec] = 300
10
+ # one hour validity (TODO: configurable?)
11
+ Factory.instance.parameters[:jwt_expiry_offset_sec] = 3600
8
12
  # Authentication using private key
13
+ # https://tools.ietf.org/html/rfc7523
14
+ # https://tools.ietf.org/html/rfc7519
9
15
  class Jwt < Base
10
- # @param g_o:private_key_obj [M] for type :jwt
11
- # @param g_o:payload [M] for type :jwt
12
- # @param g_o:headers [0] for type :jwt
16
+ # @param private_key_obj private key object
17
+ # @param payload payload to be included in the JWT
18
+ # @param headers headers to be included in the JWT
13
19
  def initialize(
14
- payload:,
15
20
  private_key_obj:,
21
+ payload:,
16
22
  headers: {},
17
23
  **base_params
18
24
  )
19
- Aspera.assert_type(payload, Hash){'payload'}
20
25
  Aspera.assert_type(private_key_obj, OpenSSL::PKey::RSA){'private_key_obj'}
26
+ Aspera.assert_type(payload, Hash){'payload'}
21
27
  Aspera.assert_type(headers, Hash){'headers'}
22
28
  super(**base_params)
23
29
  @private_key_obj = private_key_obj
24
- @payload = payload
30
+ @additional_payload = payload
25
31
  @headers = headers
26
- @identifiers.push(@payload[:sub])
32
+ @identifiers.push(@additional_payload[:sub])
27
33
  end
28
34
 
29
35
  def create_token
30
- # https://tools.ietf.org/html/rfc7523
31
- # https://tools.ietf.org/html/rfc7519
32
36
  require 'jwt'
33
37
  seconds_since_epoch = Time.new.to_i
34
38
  Log.log.info{"seconds=#{seconds_since_epoch}"}
35
- Aspera.assert(@payload.is_a?(Hash)){'missing JWT payload'}
36
39
  jwt_payload = {
37
- exp: seconds_since_epoch + OAuth::Factory.instance.globals[:jwt_expiry_offset_sec], # expiration time
38
- nbf: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec], # not before
39
- iat: seconds_since_epoch - OAuth::Factory.instance.globals[:jwt_accepted_offset_sec] + 1, # issued at
40
+ exp: seconds_since_epoch + OAuth::Factory.instance.parameters[:jwt_expiry_offset_sec], # expiration time
41
+ nbf: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec], # not before
42
+ iat: seconds_since_epoch - OAuth::Factory.instance.parameters[:jwt_accepted_offset_sec] + 1, # issued at
40
43
  jti: SecureRandom.uuid # JWT id
41
- }.merge(@payload)
42
- Log.log.debug{"JWT jwt_payload=[#{jwt_payload}]"}
44
+ }.merge(@additional_payload)
45
+ Log.log.debug{Log.dump(:jwt_payload, jwt_payload)}
43
46
  Log.log.debug{"private=[#{@private_key_obj}]"}
44
47
  assertion = JWT.encode(jwt_payload, @private_key_obj, 'RS256', @headers)
45
48
  Log.log.debug{"assertion=[#{assertion}]"}
@@ -4,16 +4,17 @@ require 'aspera/oauth/base'
4
4
 
5
5
  module Aspera
6
6
  module OAuth
7
+ # This class is used to create a token using a JSON body and a URL
7
8
  class UrlJson < Base
8
9
  def initialize(
9
- json:,
10
10
  url:,
11
+ json:,
11
12
  **generic_params
12
13
  )
13
14
  super(**generic_params)
14
- @json_params = json
15
- @url_params = url
16
- @identifiers.push(@json_params[:url_token])
15
+ @body = json
16
+ @query = url
17
+ @identifiers.push(@body[:url_token])
17
18
  end
18
19
 
19
20
  def create_token
@@ -21,8 +22,9 @@ module Aspera
21
22
  operation: 'POST',
22
23
  subpath: @path_token,
23
24
  headers: {'Accept' => 'application/json'},
24
- json_params: @json_params,
25
- url_params: @url_params.merge(scope: @scope) # scope is here because it may change over time (node)
25
+ query: @query.merge(scope: @scope), # scope is here because it may change over time (node)
26
+ body: @body,
27
+ body_type: :json
26
28
  )
27
29
  end
28
30
  end
@@ -11,12 +11,10 @@ module Aspera
11
11
  # if method is "graphical", then the URL will be opened with the default browser.
12
12
  class OpenApplication
13
13
  include Singleton
14
+ USER_INTERFACES = %i[text graphical].freeze
14
15
  class << self
15
- USER_INTERFACES = %i[text graphical].freeze
16
- # User Interfaces
17
- def user_interfaces; USER_INTERFACES; end
18
-
19
16
  def default_gui_mode
17
+ # assume not remotely connected on macos and windows
20
18
  return :graphical if [Environment::OS_WINDOWS, Environment::OS_X].include?(Environment.os)
21
19
  # unix family
22
20
  return :graphical if ENV.key?('DISPLAY') && !ENV['DISPLAY'].empty?
@@ -43,7 +41,7 @@ module Aspera
43
41
  uri_graphical(file_path.to_s)
44
42
  end
45
43
  end
46
- end # self
44
+ end
47
45
 
48
46
  attr_accessor :url_method
49
47
 
@@ -67,5 +65,5 @@ module Aspera
67
65
  raise StandardError, "unsupported url open method: #{@url_method}"
68
66
  end
69
67
  end
70
- end # OpenApplication
71
- end # Aspera
68
+ end
69
+ end
@@ -74,5 +74,5 @@ module Aspera
74
74
  return File.join(@folder, "#{object_id}#{FILE_SUFFIX}")
75
75
  # .gsub(/[^a-z]+/,FILE_FIELD_SEPARATOR)
76
76
  end
77
- end # PersistencyFolder
78
- end # Aspera
77
+ end
78
+ end
@@ -252,6 +252,6 @@ module Aspera
252
252
  '+repage',
253
253
  @destination_file_path])
254
254
  end
255
- end # Generator
256
- end # Preview
257
- end # Aspera
255
+ end
256
+ end
257
+ end
@@ -36,6 +36,6 @@ module Aspera
36
36
  DESCRIPTIONS.each do |opt|
37
37
  attr_accessor opt[:name]
38
38
  end
39
- end # Options
40
- end # Preview
41
- end # Aspera
39
+ end
40
+ end
41
+ end
@@ -95,7 +95,7 @@ module Aspera
95
95
  end
96
96
  false
97
97
  end
98
- end # class << self
99
- end # class Terminal
100
- end # module Preview
101
- end # module Aspera
98
+ end
99
+ end
100
+ end
101
+ end
@@ -117,6 +117,6 @@ module Aspera
117
117
  return output_file
118
118
  end
119
119
  end
120
- end # Options
121
- end # Preview
122
- end # Aspera
120
+ end
121
+ end
122
+ end
@@ -36,7 +36,11 @@ module Aspera
36
36
  def pac_dns_functions(context_host)
37
37
  context_self = '127.0.0.1'
38
38
  context_ip = nil
39
- Resolv::DNS.open{|dns|dns.each_address(context_host){|r_addr|context_ip = r_addr.to_s if r_addr.is_a?(Resolv::IPv4)}}
39
+ Resolv::DNS.open do |dns|
40
+ dns.each_address(context_host) do |r_addr|
41
+ context_ip = r_addr.to_s if r_addr.is_a?(Resolv::IPv4)
42
+ end
43
+ end
40
44
  raise "DNS name not found: #{context_host}" if context_ip.nil?
41
45
  # NOTE: Javascript code here with string inclusions
42
46
  javascript = <<END_OF_JAVASCRIPT