1
1
require "open3"
2
2
require "timeout"
3
3
require "fileutils"
4
+ require "pathname"
4
5
5
6
class CommandExecutionService
6
7
RAILS_GEN_ROOT = "/var/lib/rails-new-io/rails-env" . freeze
8
+ WORKSPACES_ROOT = "/var/lib/rails-new-io/workspaces" . freeze
7
9
RUBY_VERSION = "3.4.1" . freeze
8
10
RAILS_VERSION = "8.0.1" . freeze
9
11
BUNDLER_VERSION = "2.6.3" . freeze
@@ -54,6 +56,15 @@ class CommandExecutionService
54
56
VALID_OPTIONS = /\A --?[a-z][\w -]*\z / # Must start with letter after dash(es)
55
57
MAX_TIMEOUT = 300 # 5 minutes
56
58
59
+ ALLOWED_PATHS = [
60
+ RAILS_GEN_ROOT ,
61
+ WORKSPACES_ROOT ,
62
+ "/var/lib/rails-new-io/home" ,
63
+ "/var/lib/rails-new-io/config" ,
64
+ "/var/lib/rails-new-io/cache" ,
65
+ "/var/lib/rails-new-io/data"
66
+ ] . map { |path | Pathname . new ( path ) . freeze } . freeze
67
+
57
68
class InvalidCommandError < StandardError
58
69
attr_reader :metadata
59
70
@@ -73,7 +84,9 @@ def initialize(generated_app, logger, command = nil)
73
84
74
85
def execute
75
86
validate_command!
87
+ validate_work_directory! if @work_dir # Check existing work_dir if set
76
88
setup_work_directory
89
+ validate_work_directory! # Validate again after setup
77
90
78
91
Timeout . timeout ( MAX_TIMEOUT ) do
79
92
run_isolated_process
@@ -117,25 +130,41 @@ def validate_command!
117
130
@logger . debug ( "Command validation successful" , { command : @command } )
118
131
end
119
132
133
+ def validate_work_directory!
134
+ raise InvalidCommandError , "Work directory not set up" unless @work_dir
135
+ work_dir_path = Pathname . new ( @work_dir )
136
+
137
+ # Allow test directories (those under /tmp or /var/folders) in test environment
138
+ return if Rails . env . test? && ( work_dir_path . to_s . start_with? ( "/tmp/" ) || work_dir_path . to_s . start_with? ( "/var/folders/" ) )
139
+
140
+ # Ensure the work directory is under an allowed path
141
+ unless ALLOWED_PATHS . any? { |allowed | work_dir_path . to_s . start_with? ( allowed . to_s ) }
142
+ @logger . error ( "Invalid work directory path" , { work_dir : @work_dir } )
143
+ raise InvalidCommandError , "Invalid work directory path"
144
+ end
145
+
146
+ raise InvalidCommandError , "Work directory does not exist" unless Dir . exist? ( @work_dir )
147
+ end
148
+
120
149
def setup_work_directory
150
+ base_dir = Pathname . new ( WORKSPACES_ROOT )
151
+
121
152
@work_dir = if @command . start_with? ( "rails new" )
122
- base_dir = "/var/lib/rails-new-io/workspaces"
123
153
FileUtils . mkdir_p ( base_dir )
124
- workspace_dir_name = "workspace-#{ Time . current . to_i } -#{ SecureRandom . hex ( 4 ) } "
125
154
126
- File . join ( base_dir , workspace_dir_name ) . tap do |dir |
127
- FileUtils . mkdir_p ( dir )
128
- @generated_app . update ( workspace_path : dir )
129
- @logger . info ( "Created workspace directory" , { workspace_path : @work_dir } )
130
- end
131
- else
132
- File . join ( @generated_app . workspace_path , @generated_app . name ) . tap do |dir |
133
- @logger . info ( "Using existing workspace directory" , { workspace_path : dir } )
134
- end
135
- end
155
+ timestamp = Time . current . to_i . to_s
156
+ random_hex = SecureRandom . hex ( 4 )
157
+ workspace_dir_name = [ "workspace" , timestamp , random_hex ] . join ( "-" )
136
158
137
- if !Dir . exist? ( @work_dir )
138
- raise InvalidCommandError , "Work directory #{ @work_dir } does not exist!"
159
+ dir = base_dir . join ( workspace_dir_name )
160
+ FileUtils . mkdir_p ( dir )
161
+ @generated_app . update ( workspace_path : dir . to_s )
162
+ @logger . info ( "Created workspace directory" , { workspace_path : dir . to_s } )
163
+ dir . to_s
164
+ else
165
+ dir = Pathname . new ( @generated_app . workspace_path ) . join ( @generated_app . name )
166
+ @logger . info ( "Using existing workspace directory" , { workspace_path : dir . to_s } )
167
+ dir . to_s
139
168
end
140
169
end
141
170
@@ -161,16 +190,19 @@ def run_isolated_process
161
190
162
191
rails_cmd = "#{ RAILS_GEN_ROOT } /gems/bin/rails"
163
192
164
- command = if @command . start_with? ( "rails new" )
165
- args = @command . split [ 2 ..-1 ] . join ( " " )
166
- "#{ rails_cmd } new #{ args } "
193
+ command_args = if @command . start_with? ( "rails new" )
194
+ [ "new" , *@command . split [ 2 ..-1 ] ]
167
195
else
168
196
# For other rails commands (like app:template), don't include 'rails' in the args
169
- " #{ rails_cmd } #{ @command . split [ 1 ..-1 ] . join ( ' ' ) } "
197
+ @command . split [ 1 ..-1 ]
170
198
end
171
199
200
+ # Validate work directory one final time before execution
201
+ validate_work_directory!
202
+ options = { unsetenv_others : true , chdir : @work_dir }
203
+
172
204
Bundler . with_unbundled_env do
173
- execute_command ( env , command , buffer , error_buffer )
205
+ execute_command ( env , [ rails_cmd , * command_args ] , buffer , error_buffer , options )
174
206
end
175
207
176
208
@work_dir
@@ -201,8 +233,8 @@ def env_for_command
201
233
base_env
202
234
end
203
235
204
- def execute_command ( env , command , buffer , error_buffer )
205
- Open3 . popen3 ( env , command , chdir : @work_dir , unsetenv_others : true ) do |stdin , stdout , stderr , wait_thr |
236
+ def execute_command ( env , command_with_args , buffer , error_buffer , options )
237
+ Open3 . popen3 ( env , * command_with_args , options ) do |stdin , stdout , stderr , wait_thr |
206
238
@pid = wait_thr &.pid
207
239
208
240
stdout_thread = Thread . new do
@@ -230,7 +262,7 @@ def execute_command(env, command, buffer, error_buffer)
230
262
status : exit_status ,
231
263
output : output ,
232
264
error_buffer : error_buffer . join ( "<br>" ) ,
233
- command : command ,
265
+ command : command_with_args . join ( " " ) ,
234
266
directory : @work_dir ,
235
267
env : env
236
268
} )
0 commit comments