chiark / gitweb /
wip email templates etc.
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 5 Dec 2020 14:54:12 +0000 (14:54 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sat, 5 Dec 2020 14:54:12 +0000 (14:54 +0000)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
Makefile
server.toml
src/bin/otter.rs
src/config.rs
src/global.rs
src/imports.rs
src/nwtemplates.rs
src/spec.rs

index bc628b6f983a949d97514b5754bde7998adb168c..5d842878d65fb7d8ac34fac14995421b042e128d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -266,6 +266,7 @@ deploy: stamp/cargo.deploy-build bundled-sources assets libraries
        rsync -r --delete --exclude=\*~ library specs $(DEPLOY_BASE)/.
        rsync -r $(FILEASSETS) $(addprefix $(WASM_PACKED)/, $(WASM_ASSETS)) \
                $(DEPLOY_BASE)/assets/
+       rsync -r nwtemplates/*.tera $(DEPLOY_BASE)/nwtemplates/
        ssh -o BatchMode=true $(DEPLOY_USER) $(DEPLOY_FINISH)
        git branch -f $(DEPLOYED_BRANCH)
 
index 7d00840279fa9fa952726fc61c80e089ab894654..1282d47d259b570d8f97bce866053b4f3371f087 100644 (file)
@@ -13,6 +13,7 @@ public_url = "http://localhost:8000"
 save_dir = "/home/rustcargo/Rustup/Game/server"
 command_socket = "/home/rustcargo/Rustup/Game/server/command.socket"
 template_dir = "/home/ian/Rustup/Game/server/templates"
+nwtemplate_dir = "/home/ian/Rustup/Game/server/nwtemplates"
 bundled_sources = "/home/rustcargo/Rustup/Game/server/target/bundled-sources"
 wasm_dir = "/home/rustcargo/Rustup/Game/server/target/packed-wasm"
 
index 8cd15dd4f2677eac164db85b147054ceebc8ca85..1f7d675a1e8faed6c8d59d3ad6b00532c90ce8b9 100644 (file)
@@ -309,6 +309,13 @@ fn main() {
     
 
     let mut access = ap.refer(&mut rma.access);
+    access.metavar("EMAIL-ADDRESS").add_option(
+      &["--email"],
+      MapStore(|addr| Ok(Some(
+        TokenByEmail { addr: addr.to_string() }.into()
+      ))),
+      "send token by email, to EMAIL-ADDRESS (RFC822 recipient field syntax)"
+    );
     access.add_option(&["--url-on-stdout"],
                       StoreConst(Some(UrlOnStdout.into())),
                       "show game access url by printing to stdout");
index 854c4d6c411d2469b77779472b3d2577cb5f9e28..536abff6ef01f473c3d955cf59d5c207cd1faf5b 100644 (file)
@@ -26,6 +26,7 @@ pub struct ServerConfigSpec {
   pub sse_wildcard_url: Option<String>,
   pub rocket_workers: Option<u16>,
   pub template_dir: Option<String>,
+  pub nwtemplate_dir: Option<String>,
   pub wasm_dir: Option<String>,
   pub log: Option<toml::Value>,
   pub bundled_sources: Option<String>,
@@ -43,6 +44,7 @@ pub struct ServerConfig {
   pub sse_wildcard_url: Option<(String, String)>,
   pub rocket_workers: u16,
   pub template_dir: String,
+  pub nwtemplate_dir: String,
   pub wasm_dir: String,
   pub log: LogSpecification,
   pub bundled_sources: String,
@@ -57,8 +59,8 @@ impl TryFrom<ServerConfigSpec> for ServerConfig {
     let ServerConfigSpec {
       base_dir, save_dir, command_socket, debug,
       http_port, public_url, sse_wildcard_url, rocket_workers,
-      template_dir, wasm_dir,
-      log, bundled_sources, shapelibs,
+      template_dir, nwtemplate_dir, wasm_dir,
+      log, bundled_sources, shapelibs, sendmail,
     } = spec;
 
     let defpath = |specd: Option<String>, leaf: &str| -> String {
@@ -72,6 +74,7 @@ impl TryFrom<ServerConfigSpec> for ServerConfig {
     let command_socket  = defpath(command_socket,  "var/command.socket");
     let template_dir    = defpath(template_dir,    "assets"            );
     let wasm_dir        = defpath(wasm_dir,        "assets"            );
+    let nwtemplate_dir  = defpath(nwtemplate_dir,  "nwtemplates"       );
     let bundled_sources = defpath(bundled_sources, "bundled-sources"   );
     const DEFAULT_LIBRARY_GLOB : &str = "library/*.toml";
 
@@ -80,6 +83,10 @@ impl TryFrom<ServerConfigSpec> for ServerConfig {
       vec![ shapelib::Config1::PathGlob(glob) ]
     });
 
+    let sendmail = sendmail.unwrap_or_else(
+      || DEFAULT_SENDMAIL_PROGRAM.into()
+    );
+
     let public_url = public_url
       .trim_end_matches('/')
       .into();
@@ -116,8 +123,8 @@ impl TryFrom<ServerConfigSpec> for ServerConfig {
     ServerConfig {
       save_dir, command_socket, debug,
       http_port, public_url, sse_wildcard_url, rocket_workers,
-      template_dir, wasm_dir,
-      log, bundled_sources, shapelibs,
+      template_dir, nwtemplate_dir, wasm_dir,
+      log, bundled_sources, shapelibs, sendmail,
     }
   }
 }
index 6faca941f59817cfcd7224f1babd9aae7fad6100..ac853ced973aa7fc1fe2391ed4d6ce6c6146e2cc 100644 (file)
@@ -756,7 +756,7 @@ impl<'ig> InstanceGuard<'ig> {
                       &config().public_url.trim_end_matches("/"),
                       token.0);
     let info = AccessTokenInfo { url };
-    let report = access.deliver(&self.c.g, &gpl, &ipl, info)?;
+    let report = access.deliver(accounts, &self.c.g, &gpl, &ipl, info)?;
     report
   }
 
index 8206ee6a844099a3c0bcb5a4d345e98c2a089bc8..d1b4b045e34ea34d1cac9cb557a1a24704a02208 100644 (file)
@@ -19,15 +19,16 @@ pub use std::hash::Hash;
 pub use std::io;
 pub use std::io::ErrorKind;
 pub use std::io::{BufReader, Read, BufRead, BufWriter, Write};
-pub use std::io::{SeekFrom};
+pub use std::io::{Seek, SeekFrom};
 pub use std::iter::repeat_with;
 pub use std::iter;
 pub use std::marker::PhantomData;
 pub use std::mem;
 pub use std::num::{Wrapping, TryFromIntError};
 pub use std::ops::{Deref, DerefMut};
-pub use std::os::unix::ffi::OsStrExt;
 pub use std::os::unix;
+pub use std::os::unix::ffi::OsStrExt;
+pub use std::os::unix::process::CommandExt;
 pub use std::path::PathBuf;
 pub use std::process::{exit, Command};
 pub use std::str::FromStr;
index 393d1480c2bb96c307696f3dac54851141ba44f5..117fd2c54dee60de21ee64fb59a8907e155cd4c0 100644 (file)
@@ -4,7 +4,8 @@
 
 use crate::imports::*;
 
-use parking_lot::{const_rwlock, RwLock, MappedRwLockReadGuard};
+use parking_lot::{const_rwlock, RwLock, RwLockReadGuard};
+use parking_lot::{MappedRwLockReadGuard};
 
 static STATE : RwLock<Option<State>> = const_rwlock(None);
 
@@ -14,18 +15,26 @@ struct State {
 
 #[throws(StartupError)]
 pub fn init() {
-  let guard = STATE.write();
+  let mut guard = STATE.write();
   assert!(guard.is_none());
-  let glob = format!("{}/*.tera", config().nwtemplates);
-  *guard = State {
-    tera: tera::Tera::new(&glob)?,
-  };
+  let config = config();
+  let nwtemplate_dir = &config.nwtemplate_dir;
+  let glob = format!("{}/*.tera", nwtemplate_dir);
+  let tera = tera::Tera::new(&glob)
+    .map_err(|e| anyhow!("{}", e))
+    .context("load tamplates")
+    .with_context(|| nwtemplate_dir.to_string())?;
+
+  *guard = Some(State {
+    tera,
+  })
 }
 
 #[throws(tera::Error)]
-pub fn render<D: Serialize>(template_name: &str, data: &D) {
-  fn get_st() -> MappedRwLockReadGuard<'static, State> {
-    STATE.read().as_ref().unwrap()
+pub fn render<D: Serialize>(template_name: &str, data: &D) -> String {
+  fn get_tera() -> MappedRwLockReadGuard<'static, tera::Tera> {
+    let g = STATE.read();
+    RwLockReadGuard::map(g, |g| &g.as_ref().unwrap().tera)
   }
-  get_st().render(template_name, data)
+  get_tera().render(template_name, data)?
 }
index 3afb86e457f70c94648cabd4302c363e084d1521..35ed10681bcffeb7f7663e1657e45929f3573ac6 100644 (file)
@@ -331,6 +331,7 @@ pub mod implementation {
     fn check_spec_permission(&self, _: Option<AuthorisationSuperuser>) {
     }
     fn deliver(&self,
+               ag: &AccountsGuard,
                g: &Instance,
                gpl: &GPlayerState,
                ipl: &IPlayerState,
@@ -346,6 +347,7 @@ pub mod implementation {
   impl PlayerAccessSpec for PlayerAccessUnset {
     #[throws(TokenDeliveryError)]
     fn deliver(&self,
+               _ag: &AccountsGuard,
                _g: &Instance,
                _gpl: &GPlayerState,
                _ipl: &IPlayerState,
@@ -368,7 +370,8 @@ pub mod implementation {
     }
     #[throws(TokenDeliveryError)]
     fn deliver(&self,
-                   _g: &Instance,
+               _ag: &AccountsGuard,
+               _g: &Instance,
                _gpl: &GPlayerState,
                _ipl: &IPlayerState,
                _token: AccessTokenInfo) -> AccessTokenReport {
@@ -380,6 +383,7 @@ pub mod implementation {
   impl PlayerAccessSpec for UrlOnStdout {
     #[throws(TDE)]
     fn deliver<'t>(&self,
+                   _ag: &AccountsGuard,
                    _g: &Instance,
                    _gpl: &GPlayerState,
                    _ipl: &IPlayerState,
@@ -393,6 +397,7 @@ pub mod implementation {
   impl PlayerAccessSpec for TokenByEmail {
     #[throws(TDE)]
     fn deliver<'t>(&self,
+                   ag: &AccountsGuard,
                    g: &Instance,
                    gpl: &GPlayerState,
                    ipl: &IPlayerState,
@@ -412,11 +417,13 @@ pub mod implementation {
         nick: &gpl.nick,
       };
 
-      if self.addr.find(['\r','\n'] as &[char]).is_some() {
+      if self.addr.find((&['\r','\n']) as &[char]).is_some() {
         throw!(anyhow!("email address may not contain line endings"));
       }
 
-      let message = match &ipl.account {
+      let (account, _) = ag.lookup(ipl.acctid).context("find account")?;
+      let account = &account.account;
+      let message = match &account.scope {
         AS::Unix { user } => {
           #[derive(Debug,Serialize)]
           struct Data<'r> {
@@ -430,7 +437,7 @@ pub mod implementation {
           };
           nwtemplates::render("token-unix", &data)
         }
-        other => {
+        _ => {
           #[derive(Debug,Serialize)]
           struct Data<'r> {
             account: String,
@@ -438,35 +445,40 @@ pub mod implementation {
             common: CommonData<'r>,
           };
           let data = Data {
-            account: other.to_string(),
+            account: account.to_string(),
             common,
           };
           nwtemplates::render("token-other", &data)
         }
-      }.context("render email template")?;
+      }.map_err(|e| anyhow!(e.to_string()))
+        .context("render email template")?;
 
       let messagefile = (||{
-        let messagefile = tempfile::tempfile().context("tempfile")?;
+        let mut messagefile = tempfile::tempfile().context("tempfile")?;
         messagefile.write_all(message.as_bytes()).context("write")?;
         messagefile.flush().context("flush")?;
         messagefile.seek(SeekFrom::Start(0)).context("seek")?;
-        messagefile
+        Ok::<_,AE>(messagefile)
       })().context("write email to temporary file.")?;
 
       let sendmail = &config().sendmail;
-      let command = Command::new(sendmail)
+      let mut command = Command::new(sendmail);
+      command
         .args(&["-oee","-odb","-oi","-t","--"])
-        .stdin(messagefile)
-        .pre_exec(|| unsafe {
+        .stdin(messagefile);
+      unsafe {
+        command.pre_exec(|| {
           // https://github.com/rust-lang/rust/issues/79731
           let r = libc::dup2(2,1);
-          assert_eq!(r, 1);
+          if r == 0 { Ok(()) }
+          else { Err(io::Error::last_os_error()) }
         });
+      }
       let st = command
         .status()
-        .with_context(!! format!("run sendmail ({})", sendmail))?;
+        .with_context(|| format!("run sendmail ({})", sendmail))?;
       if !st.success()  {
-        throw!(format!("sendmail ({}) failed: {} ({})", sendmail, st, st));
+        throw!(anyhow!("sendmail ({}) failed: {} ({})", sendmail, st, st));
       }
       
       AccessTokenReport { lines: vec![