Couple weeks ago I worked on making a web based shell.
Before, I started googling ”web based shell”, but couldn’t find what I was looking for. Later, I decided to start build my own with my own view.
I picked Sinatra Web Framework for its ease of use for building APIs. And here I will talk about the concept of building it.
First the tree of the application was like this:
1 2 3 4 5 6 7 8 | console `|- app.rb |- config.ru |- public/ |- views/ |- web/ `|- folder_1 |- folder_2 |
Building the API
app.rb
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 | class Console < Sinatra::Base require 'json' require 'yaml' BASE_PATH = File.expand_path(File.dirname(__FILE__) + "/web") # BASE_PATH is The folder the the console will only access # Also, enable Sinatra Cookies and Sessions get '/' do session[:current_path] = BASE_PATH haml :index end before do set_current_dir @p = env["rack.request.query_hash"] #accessing `q` and `XXX` like ?q=XXX # I dont know if theres another way end def set_current_dir #set @current for the current folder we are @current = session[:current_path] end def f_level(path1_string, path2_string) # Check http://stackoverflow.com/questions/4353410/any-function-in-ruby-determine-the-difference-between-folders-level path1, path2 = [path1_string, path2_string].map { |s| s.split(File::SEPARATOR) } minsize = [path1, path2].map(&:size).min path1.first(minsize) == path2.first(minsize) ? path1.size - path2.size : nil end get '/api/v0.1/:command' do content_type :json msg = {} # a hash for returning errors, messages etc. f = "#{@current}/#{@p['q']}" f = "#{@current}" if @p['q'].nil? unless File.exist?(f) msg['error'] = "No such file or directory - #{@p['q']} in ~/#{@current[@current.index('web'), @current.length]}" return msg.to_json end level = f_level(File.expand_path(f), BASE_PATH) if level.nil? || level < 0 || level.nil? || @p['q'] && @p['q'][0] == '/' # this prevents user to go up the folder `/web` Or that you specified in BASE_PATH msg['error'] = "Unauthorized to access #{@p['q']}" return msg.to_json end f = File.expand_path(f) case params[:command] when "pwd" ("/LOTS_OF_FOLDERS/#{@current[@current.index('web'), @current.length]}").to_json when "cd" if File.directory?(f) session[:current_path] = f session[:current_path] = BASE_PATH if @p['q'] == "" f = session[:current_path] f.to_json else msg['error'] = "cd: #{@p['q']}: Not a directory" msg.to_json end when "ls" if File.directory?(f) Dir.glob("#{f}/*").map{ |x| File.basename(x)}.to_json else msg['error'] = "ls: #{@p['q']}: Not a directory" msg.to_json end when "cat" #show file content if File.file?(f) file = File.open(f) result = "" result << file.gets.strip while !file.eof? result.strip.to_json else msg['error'] = "cat: #{@p['q']}: Is a directory" msg.to_json # "cat: No such file or directory - #{@p['q']} in #{@current}".to_json end else "invalid all" end end end |
config.ru
1 2 3 4 5 6 | require 'rubygems' require 'sinatra' require 'haml' require 'sass' require './app' run Console |
Examples that return JSON object
1 2 3 4 | /api/v0.1/pwd /api/v0.1/cd[?q=SOME_FOLDER] /api/v0.1/ls[?q=SOME_FOLDER] /api/v0.1/cat?q=SOME_FILE |
Also try the plain JSON returns of the API (use them one by one to get the concept)
- http://lab.rushthinking.com/api/v1.0/pwd
- http://lab.rushthinking.com/api/v1.0/cd
- http://lab.rushthinking.com/api/v1.0/cd?q=posts
- http://lab.rushthinking.com/api/v1.0/pwd
- http://lab.rushthinking.com/api/v1.0/cd?q=..
- http://lab.rushthinking.com/api/v1.0/ls
- http://lab.rushthinking.com/api/v1.0/cd?q=pages
- http://lab.rushthinking.com/api/v1.0/cat?q=about.page
Finally
You have to run the Sinatra app. I will release its full source on Github soon. For the Graphical user interface I used a custom modified version of JQuery Console 1.0 and some Javascript here and there! You can check a working example here http://lab.rushthinking.com.
Update
This post is about how building an API in general, I used Ruby File API, and the only methods I worked on iscd, pwd, ls, cat. Though nano, su, sudo and others will not work! Thanks Jamal Shaheen.