samagotchi 0.108.0 → 0.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,9 @@ require "webrick"
9
9
  require_relative "app"
10
10
  require_relative "info_probe"
11
11
  require_relative "lan"
12
+ require_relative "link_codes"
13
+ require_relative "proxy_gate"
14
+ require_relative "public_url"
12
15
  require_relative "qr"
13
16
  require_relative "running"
14
17
  require_relative "session_hub"
@@ -58,6 +61,10 @@ module Samagotchi
58
61
  # How long `chi web` waits for a server already on its port to answer.
59
62
  PROBE_TIMEOUT = 0.3
60
63
 
64
+ # The raw header names next to Rack's env (ProxyGate's identity check):
65
+ # chi web is the only WEBrick server chi runs.
66
+ WEBrick::HTTPRequest.prepend(ProxyGate::RawHeaderNames) unless WEBrick::HTTPRequest < ProxyGate::RawHeaderNames
67
+
61
68
  # `chi web`: one server serves every project, the scope is in the page
62
69
  # URL. If a chi web already runs on the port, print (and with --open,
63
70
  # open) its page for +dir+ and leave it be; else start one.
@@ -70,6 +77,7 @@ module Samagotchi
70
77
  editor: Config::BY_KEY["web.editor"].default, new_token: false,
71
78
  working_set: Config::BY_KEY["web.working_set"].default)
72
79
  rotate_token if new_token
80
+ public_url = public_url_setting
73
81
  setting = host_setting(host)
74
82
  lan = Lan.wanted?(setting) ? Lan.choose(setting) : nil
75
83
  host = resolve_host(setting)
@@ -85,6 +93,7 @@ module Samagotchi
85
93
  end
86
94
  puts "chi web already runs on port #{port} (pid #{found["pid"]}): #{url}"
87
95
  puts running_lan_lines(found["lan"], port) if found["lan"]
96
+ puts running_public_lines(public_url) if public_url
88
97
  puts lan_off_line(setting) if new_token && !found["lan"]
89
98
  open_url(url) if open_browser
90
99
  0
@@ -92,20 +101,21 @@ module Samagotchi
92
101
  warn in_use_message(port)
93
102
  1
94
103
  else
95
- if new_token && !lan
104
+ if new_token && !lan && !public_url
96
105
  puts lan_off_line(setting)
97
106
  return 0
98
107
  end
99
108
  # The participants' grants: never served from a store others can read.
100
109
  Grants.check_permissions!(Grants.path)
101
110
  if start(port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, view: view,
102
- annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set)
111
+ annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set,
112
+ public_url: public_url)
103
113
  0
104
114
  else
105
115
  1
106
116
  end
107
117
  end
108
- rescue Lan::Error, Grants::Insecure => e
118
+ rescue Lan::Error, Grants::Insecure, PublicUrl::Invalid => e
109
119
  warn "Error: #{e.message}"
110
120
  1
111
121
  rescue Interrupt
@@ -120,11 +130,13 @@ module Samagotchi
120
130
  # @param lan [Lan::Choice, nil] also listen on this LAN address, where
121
131
  # every request but this machine's needs the access token
122
132
  # @param token_path [String] the LAN access token's file
133
+ # @param public_url [PublicUrl, nil] behind an auth proxy
134
+ # (web.public_url): every request needs the access token
123
135
  # @return [Boolean] false when the port was taken (said so on stderr)
124
136
  def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
125
137
  view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
126
138
  editor: Config::BY_KEY["web.editor"].default, token_path: Token.path,
127
- working_set: Config::BY_KEY["web.working_set"].default)
139
+ working_set: Config::BY_KEY["web.working_set"].default, public_url: nil)
128
140
  config_restart_unsubscribe = ConfigFile.watch_restart_settings!(process: :web) do |message|
129
141
  Samagotchi::Log.warn(:config, "restart_required", echo: "Warning: config: #{message}")
130
142
  end
@@ -133,14 +145,18 @@ module Samagotchi
133
145
  url ||= "http://#{url_host(host)}:#{port}/"
134
146
 
135
147
  hub ||= SessionHub.new(state_dir: state_dir || Session.default_state_dir, manager: manager || SessionManager)
136
- if lan
137
- Token.load_or_create(token_path)
138
- lan_option = { ip: lan.ip, token: Token::Source.new(token_path) }
139
- end
148
+ Token.load_or_create(token_path) if lan || public_url
149
+ lan_option = { ip: lan.ip, token: Token::Source.new(token_path) } if lan
150
+ public_token = Token::Source.new(token_path) if public_url
140
151
  app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, view: view,
141
- annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option, working_set: working_set)
152
+ annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option, working_set: working_set,
153
+ public_url: public_url, public_token: public_token)
142
154
  Samagotchi::Log.info(:web, "start", url: "http://#{host}:#{port}", version: Samagotchi::VERSION)
143
155
  Samagotchi::Log.info(:web, "lan", ip: lan.ip, interface: lan.interface) if lan
156
+ if public_url
157
+ Samagotchi::Log.info(:web, "public_url", origin: public_url.origin, identity_header: public_url.identity_label,
158
+ owner: public_url.owner ? "set" : "none")
159
+ end
144
160
  # Subscribed before the first scan, which makes the first check.
145
161
  hub.subscribe(method(:on_hub_event))
146
162
  hub.start
@@ -151,12 +167,15 @@ module Samagotchi
151
167
  record_running(running_path, port)
152
168
  puts "Chi Web on #{url} (public: #{File.expand_path("public", __dir__)})"
153
169
  puts lan_lines(ip: lan.ip, port: port, token: Token.read(token_path), others: lan.others, public: lan.public) if lan
170
+ puts public_lines(public_url, token: Token.read(token_path), codes: owner_codes(state_dir)) if public_url
154
171
  puts "Press Ctrl-C to stop."
155
172
  $stdout.flush # a log file isn't line-buffered
156
173
  if open_browser
174
+ # With web.public_url the page opens through the proxy.
175
+ open_target = public_url ? public_url.link(owner_codes(state_dir).issue(kind: :owner, ttl: LinkCodes::OWNER_TTL)) : url
157
176
  Thread.new do
158
177
  sleep 0.8
159
- open_url(url)
178
+ open_url(open_target)
160
179
  end
161
180
  end
162
181
  end
@@ -224,6 +243,47 @@ module Samagotchi
224
243
  raise LanListenError, e.message
225
244
  end
226
245
 
246
+ # web.public_url as Server runs it, nil when unset.
247
+ # @raise [PublicUrl::Invalid]
248
+ def self.public_url_setting
249
+ PublicUrl.from_config(url: Samagotchi::Config.get("web.public_url"),
250
+ identity_header: Samagotchi::Config.get("web.proxy.identity_header"),
251
+ owner: Samagotchi::Config.get("web.proxy.owner"))
252
+ end
253
+
254
+ # What web.public_url adds to the start lines: the proxied link (the
255
+ # token in its fragment) and who the owner is. On this machine the
256
+ # page opens through the proxy too; a direct request takes the token
257
+ # as Authorization: Bearer only (no cookie for 127.0.0.1, which every
258
+ # local port would get).
259
+ def self.public_lines(public_url, token:, codes: owner_codes)
260
+ unless token
261
+ return ["Proxied: #{public_url.origin}/ (no access token: its file is gone; chi web --new-token makes one)"]
262
+ end
263
+
264
+ who = if public_url.identity_header.nil? then "token only, no identity header"
265
+ elsif public_url.owner then "owner #{public_url.owner} by #{public_url.identity_header}, and the token"
266
+ else "nobody is the owner by identity: web.proxy.owner is unset"
267
+ end
268
+ code = codes.issue(kind: :owner, ttl: LinkCodes::OWNER_TTL)
269
+ ["Proxied: #{public_url.link(code)} ← opens chi web as you once, within #{LinkCodes::OWNER_TTL / 60} minutes " \
270
+ "(chi web again prints a new one)",
271
+ " (#{who}; on this machine the page opens through the proxy too)"]
272
+ end
273
+
274
+ # The owner's one-time link codes, next to the grants.
275
+ def self.owner_codes(state_dir = nil) = LinkCodes.for_grants(Grants.path(state_dir: state_dir))
276
+
277
+ # A second chi web, when this machine's config has web.public_url: the
278
+ # link from this config and the token file, never from what answered
279
+ # on the port.
280
+ def self.running_public_lines(public_url, token_path: Token.path, codes: owner_codes)
281
+ return ["Proxied: #{public_url.origin}/"] unless Token.read(token_path)
282
+
283
+ ["Proxied: #{public_url.link(codes.issue(kind: :owner, ttl: LinkCodes::OWNER_TTL))} (once, within " \
284
+ "#{LinkCodes::OWNER_TTL / 60} minutes)"]
285
+ end
286
+
227
287
  def self.host_setting(host)
228
288
  (host || Samagotchi::Config.get("web.host")).to_s.strip
229
289
  end
@@ -70,14 +70,15 @@ module Samagotchi
70
70
  def initialize(port:, bind: nil, session_id: nil, started_at: nil, version: nil, input_format: nil, features: [],
71
71
  epoch: nil) = super
72
72
 
73
- # Written whole or not at all; input_format and epoch only when they
74
- # name one.
73
+ # Written whole or not at all, readable by this user only (0600: no
74
+ # other account learns the worker's port from it); input_format and
75
+ # epoch only when they name one.
75
76
  def write(session_dir)
76
77
  FileUtils.mkdir_p(session_dir)
77
78
  record = to_h.transform_keys(&:to_s)
78
79
  record.delete("input_format") if input_format.nil?
79
80
  record.delete("epoch") if epoch.nil?
80
- AtomicFile.write(self.class.path(session_dir), "#{JSON.pretty_generate(record)}\n")
81
+ AtomicFile.write(self.class.path(session_dir), "#{JSON.pretty_generate(record)}\n", perm: 0o600)
81
82
  end
82
83
  end
83
84
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.108.0
4
+ version: 0.109.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -497,6 +497,7 @@ files:
497
497
  - lib/samagotchi/prompt_literal_guard.rb
498
498
  - lib/samagotchi/prompt_note.rb
499
499
  - lib/samagotchi/prompt_warmup.rb
500
+ - lib/samagotchi/proxy_trace.rb
500
501
  - lib/samagotchi/question_answer.rb
501
502
  - lib/samagotchi/question_asked.rb
502
503
  - lib/samagotchi/question_desk.rb
@@ -697,6 +698,7 @@ files:
697
698
  - lib/samagotchi/web/grants.rb
698
699
  - lib/samagotchi/web/info_probe.rb
699
700
  - lib/samagotchi/web/lan.rb
701
+ - lib/samagotchi/web/link_codes.rb
700
702
  - lib/samagotchi/web/markdown_renderer.rb
701
703
  - lib/samagotchi/web/participant_access.rb
702
704
  - lib/samagotchi/web/participant_review.rb
@@ -704,6 +706,7 @@ files:
704
706
  - lib/samagotchi/web/participant_view.rb
705
707
  - lib/samagotchi/web/participants_admin.rb
706
708
  - lib/samagotchi/web/principal.rb
709
+ - lib/samagotchi/web/proxy_gate.rb
707
710
  - lib/samagotchi/web/public/activity.js
708
711
  - lib/samagotchi/web/public/annotate_actions.js
709
712
  - lib/samagotchi/web/public/annotate_bar.js
@@ -711,6 +714,7 @@ files:
711
714
  - lib/samagotchi/web/public/annotations.js
712
715
  - lib/samagotchi/web/public/answer_refs.js
713
716
  - lib/samagotchi/web/public/app.js
717
+ - lib/samagotchi/web/public/auth_lost.js
714
718
  - lib/samagotchi/web/public/bubbles.js
715
719
  - lib/samagotchi/web/public/card.js
716
720
  - lib/samagotchi/web/public/cards_place.js
@@ -799,6 +803,7 @@ files:
799
803
  - lib/samagotchi/web/public/timing.js
800
804
  - lib/samagotchi/web/public/toast_place.js
801
805
  - lib/samagotchi/web/public/toasts.js
806
+ - lib/samagotchi/web/public/token_fragment.js
802
807
  - lib/samagotchi/web/public/tokens.css
803
808
  - lib/samagotchi/web/public/toolkit_chip.js
804
809
  - lib/samagotchi/web/public/turn_events.js
@@ -815,6 +820,7 @@ files:
815
820
  - lib/samagotchi/web/public/working_set.js
816
821
  - lib/samagotchi/web/public/working_set_rail.js
817
822
  - lib/samagotchi/web/public/working_set_view.js
823
+ - lib/samagotchi/web/public_url.rb
818
824
  - lib/samagotchi/web/qr.rb
819
825
  - lib/samagotchi/web/running.rb
820
826
  - lib/samagotchi/web/server.rb