tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

server.tri (8319B)


      1 !import prelude *
      2 !import io *
      3 !import http *
      4 !import socket *
      5 !import native_math as Nat
      6 !export *
      7 
      8 -- Store root is trusted configuration. These pure path helpers are not guards;
      9 -- every read/write entrypoint must validate a hash before using it as a path.
     10 pathJoin parent child = append parent (append "/" child)
     11 objectDir root shard = pathJoin (pathJoin root "objects") shard
     12 
     13 hashShard [a | [b | [c | _]]] = [a b c]
     14 hashShard [a | [b | _]] = [a b]
     15 hashShard [a | _] = [a]
     16 hashShard _ = []
     17 
     18 bundleObjectPath root hash = pathJoin (objectDir root (hashShard hash)) (append hash ".arboricx")
     19 
     20 -- Exact canonical lowercase SHA-256 spelling. Literal patterns, unlike numeric
     21 -- range predicates, reject malformed numeral trees without normalizing them.
     22 bundleHexByte? 48 = true
     23 bundleHexByte? 49 = true
     24 bundleHexByte? 50 = true
     25 bundleHexByte? 51 = true
     26 bundleHexByte? 52 = true
     27 bundleHexByte? 53 = true
     28 bundleHexByte? 54 = true
     29 bundleHexByte? 55 = true
     30 bundleHexByte? 56 = true
     31 bundleHexByte? 57 = true
     32 bundleHexByte? 97 = true
     33 bundleHexByte? 98 = true
     34 bundleHexByte? 99 = true
     35 bundleHexByte? 100 = true
     36 bundleHexByte? 101 = true
     37 bundleHexByte? 102 = true
     38 bundleHexByte? _ = false
     39 
     40 -- The counter is internal and canonical. At zero inspect only the list shape:
     41 -- never force the 65th byte or its suffix, even for arbitrary malformed trees.
     42 bundleHashBytes [] 0 = true
     43 bundleHashBytes _ 0 = false
     44 bundleHashBytes [byte | rest] remaining = case (bundleHexByte? byte) of
     45   ^true = bundleHashBytes rest (Nat.subtract remaining 1)
     46   _ = false
     47 
     48 bundleHashBytes _ _ = false
     49 
     50 bundleHashValid? hash = bundleHashBytes hash 64
     51 bundleHashC = guardC "expected 64 lowercase hexadecimal hash characters" bundleHashValid?
     52 
     53 -- Directory creation already tolerates existing directories in the Haskell
     54 -- driver. Propagate real failures and stop; void would discard those failures.
     55 ensureDir path = mapErrIO "createDirectory: " (createDirectory path)
     56 ensureStore root = (do onOk_
     57   ensureDir root
     58   ensureDir (pathJoin root "tmp")
     59   ensureDir (pathJoin root "objects")
     60   ensureDir (pathJoin root "aliases")
     61   ensureDir (pathJoin (pathJoin root "aliases") "names")
     62   ensureDir (pathJoin (pathJoin root "aliases") "packages")
     63   ensureDir (pathJoin root "manifests")
     64   pure (ok t t))
     65 
     66 -- Hash opaque bytes ourselves: no public unchecked hash-taking writer. Validate
     67 -- even the digest reply before constructing filesystem actions. Atomic writes
     68 -- use host-managed unique staging in the destination directory, not hash.tmp.
     69 -- Existing-object replacement remains supported. A post-publication sync error
     70 -- can leave the new object visible; this is not rollback or bundle validation.
     71 putBundle root bytes = (do onOk_
     72   hash <- mapErrIO "sha256Hex: " (sha256Hex bytes)
     73   case (checkContract bundleHashC hash) of
     74     ((t t), (value, _)) = (do onOk_
     75       ensureDir (objectDir root (hashShard value))
     76       mapErrIO "atomicWriteBytes: " (atomicWriteBytes (bundleObjectPath root value) bytes)
     77       pure (ok value t))
     78     (t, (diagnostic, _)) = pure (err (append "sha256Hex: " diagnostic) t)
     79     _ = pure (err "sha256Hex: Invalid hash contract result" t))
     80 
     81 -- Translate contract failures before IO, preserving HTTP status/message roles.
     82 getBundleByHash root hash = case (checkContract bundleHashC hash) of
     83   ((t t), (value, _)) = (do bind
     84     reply <- readFile (bundleObjectPath root value)
     85     case reply of
     86       ((t t), (bytes, _)) = pure (ok bytes t)
     87       (t, (message, _)) = pure (err 404 message)
     88       _ = pure (err 404 "Invalid read result"))
     89   (t, (diagnostic, _)) = pure (err 400 diagnostic)
     90   _ = pure (err 400 "Invalid hash contract result")
     91 
     92 -- A proper list prefix; return its exact suffix, without normalizing the tail.
     93 stripPrefix [] input = just input
     94 stripPrefix [wanted | prefix] [byte | rest] = case byte of
     95   ^wanted = stripPrefix prefix rest
     96   _ = nothing
     97 
     98 stripPrefix _ _ = nothing
     99 
    100 bundleHashPrefix = "/_arboricx/bundle/hash/"
    101 bundlePath = "/_arboricx/bundle"
    102 healthPath = "/_arboricx/health"
    103 bundleContentType = "application/vnd.arboricx.bundle"
    104 
    105 htmlLandingPage = "<!DOCTYPE html><html><head><meta name='viewport' content='width=device-width, initial-scale=1'><title>Arboricx Server</title></head><body><h1>Arboricx Server</h1><p>Bundle registry</p><p><a href='https://git.eversole.co/James/tricu'>Made with Love (and trees, lots of trees)</a></p></body></html>"
    106 
    107 bundleResponse bytes = response 200 bundleContentType bytes
    108 
    109 serveBundleHash root hash = (do bind
    110   reply <- getBundleByHash root hash
    111   case reply of
    112     ((t t), (bytes, _)) = pure (bundleResponse bytes)
    113     (t, (status, message)) = pure (errorResponse status message)
    114     _ = pure (errorResponse 500 "Invalid bundle result"))
    115 
    116 healthRoute "GET" ^healthPath = pure (okResponse "OK\n")
    117 healthRoute _ _ = pure notFoundResponse
    118 
    119 putBundleRoute root "POST" ^bundlePath body = (do bind
    120   reply <- putBundle root body
    121   case reply of
    122     ((t t), (hash, _)) = pure (createdResponse hash)
    123     (t, (message, _)) = pure (badRequestResponse (append "Upload failed: " message))
    124     _ = pure (errorResponse 500 "Invalid upload result"))
    125 
    126 putBundleRoute _ _ _ _ = pure notFoundResponse
    127 
    128 getBundleRoute root "GET" target = case (stripPrefix bundleHashPrefix target) of
    129   (t hash) = serveBundleHash root hash
    130   _ = pure notFoundResponse
    131 
    132 getBundleRoute _ _ _ = pure notFoundResponse
    133 
    134 -- Read-only exact-version index. Administrative publication happens locally;
    135 -- this route never resolves dependencies or executes fetched code. HTTP input
    136 -- bytes are canonical naturals. Scan at most 128 bytes per path component.
    137 packagePrefix = "/_arboricx/package/"
    138 packageAlphaNum? byte = (or? (Nat.inRange byte 48 57)
    139   (or? (Nat.inRange byte 65 90) (Nat.inRange byte 97 122)))
    140 packageByte? byte = (or? (packageAlphaNum? byte)
    141   (or? (equal? byte 46) (or? (equal? byte 95) (equal? byte 45))))
    142 packageVersion [] reversed _ = just (reverse reversed)
    143 packageVersion _ _ 0 = nothing
    144 packageVersion [byte | rest] reversed remaining = case (packageByte? byte) of
    145   ^true = packageVersion rest (pair byte reversed) (Nat.subtract remaining 1)
    146   _ = nothing
    147 
    148 packageVersion _ _ _ = nothing
    149 packageName [47 | [first | rest]] reversed _ = case (packageAlphaNum? first) of
    150   ^true = case (packageVersion rest [first] 127) of
    151     (t version) = just (pair (reverse reversed) version)
    152     _ = nothing
    153   _ = nothing
    154 
    155 packageName _ _ 0 = nothing
    156 packageName [byte | rest] reversed remaining = case (packageByte? byte) of
    157   ^true = packageName rest (pair byte reversed) (Nat.subtract remaining 1)
    158   _ = nothing
    159 
    160 packageName _ _ _ = nothing
    161 packageTarget [first | rest] = case (packageAlphaNum? first) of
    162   ^true = packageName rest [first] 127
    163   _ = nothing
    164 
    165 packageTarget _ = nothing
    166 
    167 servePackage root target = case (packageTarget target) of
    168   (t (name, version)) = (do bind
    169     reply <- readFile (pathJoin (pathJoin (pathJoin root "packages") name) version)
    170     case reply of
    171       ((t t), (bytes, _)) = pure (response 200 "application/vnd.arboricx.package-release" bytes)
    172       _ = pure (errorResponse 404 "Package release not found"))
    173   _ = pure (badRequestResponse "Invalid package name/version")
    174 
    175 getRegistryRoute root target = case (stripPrefix packagePrefix target) of
    176   (t suffix) = servePackage root suffix
    177   _ = getBundleRoute root "GET" target
    178 
    179 arboricxRouter _ "GET" "/" _ _ = pure (htmlResponse htmlLandingPage)
    180 arboricxRouter _ "GET" ^healthPath _ _ = healthRoute "GET" healthPath
    181 arboricxRouter root "GET" target _ _ = getRegistryRoute root target
    182 arboricxRouter root "POST" target _ body = putBundleRoute root "POST" target body
    183 arboricxRouter _ _ _ _ _ = pure notFoundResponse
    184 
    185 arboricxHandler root client peer = httpHandlerIO (arboricxRouter root) client peer
    186 
    187 -- Close a newly acquired socket on ordinary bind/listen failures. As elsewhere,
    188 -- finally does not catch terminal abortIO or host exceptions; successful serving
    189 -- delegates scheduling/accept policy to serveForever, without a shutdown hook.
    190 arboricxServer root addr port = (let
    191   failed message = pure (err message t)
    192   opened server = (let
    193     rejected message = finally (failed message) (closeSocket_ server)
    194     ready _ = serveForever server (arboricxHandler root)
    195     in onResult_ (do onOk_
    196       bindSocket server addr port
    197       listen server 128)
    198       rejected ready)
    199   in onResult_ socket failed opened)