Posted on Feb 14, 2011

Real Web Based Console

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 nanosusudo and others will not work! Thanks Jamal Shaheen.

blog comments powered by Disqus