yahns Ruby server user/dev discussion
 help / color / Atom feed
238edd9eca063c9b340e99e632983a3f0082b0b3 blob 5416 bytes (raw)

  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
 
# -*- encoding: binary -*-
# Copyright (C) 2013-2016 all contributors <yahns-public@yhbt.net>
# License: GPL-3.0+ (https://www.gnu.org/licenses/gpl-3.0.txt)
# frozen_string_literal: true
require 'time'
require 'rack/utils'
require 'rack/request'

# this is middleware meant to behave like "index" and "autoindex" in nginx
# No CSS or JS to avoid potential security bugs
# Only basic pre-formatted HTML, not even tables, should look good in lynx
# all bikeshedding here :>
class Autoindex
  FN = %{<a href="%s">%s</a>}
  TFMT = "%Y-%m-%d %H:%M"

  def initialize(app, *args)
    app.respond_to?(:root) or raise ArgumentError,
       "wrapped app #{app.inspect} does not respond to #root"
    @app = app
    @root = app.root

    @index = case args[0]
    when Array then args.shift
    when String then Array(args.shift)
    else
      %w(index.html)
    end

    @skip_gzip_static = @skip_dotfiles = nil
    case args[0]
    when Hash
      @skip_gzip_static = args[0][:skip_gzip_static]
      @skip_dotfiles = args[0][:skip_dotfiles]
    when true, false
      @skip_gzip_static = args.shift
    end
    @skip_gzip_static = true if @skip_gzip_static.nil?
    @skip_dotfiles = false if @skip_dotfiles.nil?
  end

  def redirect_slash(env)
    req = Rack::Request.new(env)
    location = "#{req.url}/"
    body = "Redirecting to #{location}\n"
    [ 302,
      {
        "Content-Type" => "text/plain",
        "Location" => location,
        "Content-Length" => body.size.to_s
      },
     [ body ] ]
  end

  def call(env)
    case env["REQUEST_METHOD"]
    when "GET", "HEAD"
      # try to serve the static file, first
      status, headers, body = res = @app.call(env)
      return res if status.to_i != 404

      path_info = env["PATH_INFO"]
      path_info_ue = Rack::Utils.unescape(path_info, Encoding::BINARY)

      # reject requests to go up a level (browser takes care of it)
      path_info_ue =~ /\.\./ and return r(403)

      # cleanup the path
      path_info_ue.squeeze!('/')

      # will raise ENOENT/ENOTDIR
      pfx = "#@root#{path_info_ue}"
      dir = Dir.open(pfx)

      return redirect_slash(env) unless path_info =~ %r{/\z}

      # try index.html and friends
      tryenv = env.dup
      @index.each do |base|
        tryenv["PATH_INFO"] = "#{path_info}#{base}"
        status, headers, body = res = @app.call(tryenv)
        return res if status.to_i != 404
      end

      # generate the index, show directories first
      dirs = []
      files = []
      ngz_idx = {} if @skip_gzip_static # used to avoid redundant stat()
      dir.each do |base|
        case base
        when "."
          next
        when ".."
          next if path_info == "/"
        when /\A\./
          next if @skip_dotfiles
        end

        begin
          st = File.stat("#{pfx}#{base}")
        rescue
          next
        end

        url = Rack::Utils.escape_html(Rack::Utils.escape(base))
        name = Rack::Utils.escape_html(base)
        if st.directory?
          name << "/"
          url << "/"
        end
        entry = sprintf(FN, url, name)
        pad = 52 - name.size
        entry << (" " * pad) if pad > 0
        entry << st.mtime.strftime(TFMT)
        entry << sprintf("% 8s", human_size(st))
        entry = [name, entry]

        if st.directory?
          dirs << entry
        elsif ngz_idx
          ngz_idx[name] = entry
        else
          files << entry
        end
      end

      if ngz_idx
        ngz_idx.each do |name, entry|
          # n.b: use use dup.sub! to ensure ngz_path is nil
          # if .gz is not found
          ngz_path = name.dup.sub!(/\.gz\z/, '')
          ngz_idx.include?(ngz_path) or files << entry
        end
      end

      dirs.sort! { |(a,_),(b)| a <=> b }.map! { |(_,ent)| ent }
      files.sort! { |(a,_),(b)| a <=> b }.map! { |(_,ent)| ent }

      path_info_html = path_info_ue.split(%r{/}, -1).map! do |part|
        Rack::Utils.escape_html(part)
      end.join("/")
      body = "<html><head><title>Index of #{path_info_html}</title></head>" \
             "<body><h1>Index of #{path_info_html}</h1><hr><pre>\n" \
             "#{dirs.concat(files).join("\n")}" \
             "</pre><hr></body></html>\n"
      h = { "Content-Type" => "text/html", "Content-Length" => body.size.to_s }
      [ 200, h, [ body ] ]
    else
      r(405)
    end
  rescue Errno::ENOENT, Errno::ENOTDIR # from Dir.open
    r(404)
  rescue => e
    r(500, e, env)
  ensure
    dir.close if dir
  end

  def r(code, exc = nil, env = nil)
    if env && exc && logger = env["rack.logger"]
      msg = exc.message
      msg = msg.dump if /[[:cntrl:]]/ =~ msg # prevent code injection
      logger.warn("#{env['REQUEST_METHOD']} #{env['PATH_INFO']} " \
                  "#{code} #{msg}")
      exc.backtrace.each { |line| logger.warn(line) }
    end

    if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(code)
      [ code, {}, [] ]
    else
      msg = "#{code} #{Rack::Utils::HTTP_STATUS_CODES[code.to_i]}\n"
      h = { 'Content-Type' => 'text/plain', 'Content-Length' => msg.size.to_s }
      [ code, h, [ msg ] ]
    end
  end

  def human_size(st)
    if st.file?
      size = st.size
      suffix = ""
      %w(K M G T).each do |s|
        break if size < 1024
        size /= 1024.0
        if size <= 1024
          suffix = s
          break
        end
      end
      "#{size.round}#{suffix}"
    else
      "-"
    end
  end
end
debug log:

solving 238edd9 ...
found 238edd9 in https://yhbt.net/yahns.git

yahns Ruby server user/dev discussion

Archives are clonable:
	git clone --mirror https://yhbt.net/yahns-public
	git clone --mirror http://ou63pmih66umazou.onion/yahns-public

Example config snippet for mirrors

Newsgroups are available over NNTP:
	nntp://news.public-inbox.org/inbox.comp.lang.ruby.yahns
	nntp://ou63pmih66umazou.onion/inbox.comp.lang.ruby.yahns

 note: .onion URLs require Tor: https://www.torproject.org/

AGPL code for this site: git clone https://public-inbox.org/public-inbox.git