caddy.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650
  1. // Package caddy provides a PHP module for the Caddy web server.
  2. // FrankenPHP embeds the PHP interpreter directly in Caddy, giving it the ability to run your PHP scripts directly.
  3. // No PHP FPM required!
  4. package caddy
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "github.com/caddyserver/caddy/v2"
  14. "github.com/caddyserver/caddy/v2/caddyconfig"
  15. "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
  16. "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
  17. "github.com/caddyserver/caddy/v2/modules/caddyhttp"
  18. "github.com/caddyserver/caddy/v2/modules/caddyhttp/fileserver"
  19. "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
  20. "github.com/dunglas/frankenphp"
  21. "go.uber.org/zap"
  22. )
  23. const defaultDocumentRoot = "public"
  24. func init() {
  25. caddy.RegisterModule(FrankenPHPApp{})
  26. caddy.RegisterModule(FrankenPHPModule{})
  27. httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
  28. httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
  29. httpcaddyfile.RegisterDirectiveOrder("php", "before", "file_server")
  30. httpcaddyfile.RegisterDirective("php_server", parsePhpServer)
  31. httpcaddyfile.RegisterDirectiveOrder("php_server", "before", "file_server")
  32. }
  33. type mainPHPinterpreterKeyType int
  34. var mainPHPInterpreterKey mainPHPinterpreterKeyType
  35. var phpInterpreter = caddy.NewUsagePool()
  36. type phpInterpreterDestructor struct{}
  37. func (phpInterpreterDestructor) Destruct() error {
  38. frankenphp.Shutdown()
  39. return nil
  40. }
  41. type workerConfig struct {
  42. // FileName sets the path to the worker script.
  43. FileName string `json:"file_name,omitempty"`
  44. // Num sets the number of workers to start.
  45. Num int `json:"num,omitempty"`
  46. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  47. Env map[string]string `json:"env,omitempty"`
  48. }
  49. type FrankenPHPApp struct {
  50. // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs.
  51. NumThreads int `json:"num_threads,omitempty"`
  52. // Workers configures the worker scripts to start.
  53. Workers []workerConfig `json:"workers,omitempty"`
  54. }
  55. // CaddyModule returns the Caddy module information.
  56. func (a FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
  57. return caddy.ModuleInfo{
  58. ID: "frankenphp",
  59. New: func() caddy.Module { return &a },
  60. }
  61. }
  62. func (f *FrankenPHPApp) Start() error {
  63. repl := caddy.NewReplacer()
  64. logger := caddy.Log()
  65. opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)}
  66. for _, w := range f.Workers {
  67. opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env))
  68. }
  69. _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
  70. if err := frankenphp.Init(opts...); err != nil {
  71. return nil, err
  72. }
  73. return phpInterpreterDestructor{}, nil
  74. })
  75. if err != nil {
  76. return err
  77. }
  78. if loaded {
  79. frankenphp.Shutdown()
  80. if err := frankenphp.Init(opts...); err != nil {
  81. return err
  82. }
  83. }
  84. return nil
  85. }
  86. func (*FrankenPHPApp) Stop() error {
  87. caddy.Log().Info("FrankenPHP stopped 🐘")
  88. return nil
  89. }
  90. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  91. func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  92. for d.Next() {
  93. for d.NextBlock(0) {
  94. switch d.Val() {
  95. case "num_threads":
  96. if !d.NextArg() {
  97. return d.ArgErr()
  98. }
  99. v, err := strconv.Atoi(d.Val())
  100. if err != nil {
  101. return err
  102. }
  103. f.NumThreads = v
  104. case "worker":
  105. wc := workerConfig{}
  106. if d.NextArg() {
  107. wc.FileName = d.Val()
  108. }
  109. if d.NextArg() {
  110. v, err := strconv.Atoi(d.Val())
  111. if err != nil {
  112. return err
  113. }
  114. wc.Num = v
  115. }
  116. for d.NextBlock(1) {
  117. v := d.Val()
  118. switch v {
  119. case "file":
  120. if !d.NextArg() {
  121. return d.ArgErr()
  122. }
  123. wc.FileName = d.Val()
  124. case "num":
  125. if !d.NextArg() {
  126. return d.ArgErr()
  127. }
  128. v, err := strconv.Atoi(d.Val())
  129. if err != nil {
  130. return err
  131. }
  132. wc.Num = v
  133. case "env":
  134. args := d.RemainingArgs()
  135. if len(args) != 2 {
  136. return d.ArgErr()
  137. }
  138. if wc.Env == nil {
  139. wc.Env = make(map[string]string)
  140. }
  141. wc.Env[args[0]] = args[1]
  142. }
  143. if wc.FileName == "" {
  144. return errors.New(`The "file" argument must be specified`)
  145. }
  146. if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(wc.FileName) {
  147. wc.FileName = filepath.Join(frankenphp.EmbeddedAppPath, wc.FileName)
  148. }
  149. }
  150. f.Workers = append(f.Workers, wc)
  151. }
  152. }
  153. }
  154. return nil
  155. }
  156. func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
  157. app := &FrankenPHPApp{}
  158. if err := app.UnmarshalCaddyfile(d); err != nil {
  159. return nil, err
  160. }
  161. // tell Caddyfile adapter that this is the JSON for an app
  162. return httpcaddyfile.App{
  163. Name: "frankenphp",
  164. Value: caddyconfig.JSON(app, nil),
  165. }, nil
  166. }
  167. type FrankenPHPModule struct {
  168. // Root sets the root folder to the site. Default: `root` directive, or the path of the public directory of the embed app it exists.
  169. Root string `json:"root,omitempty"`
  170. // SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`.
  171. SplitPath []string `json:"split_path,omitempty"`
  172. // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists.
  173. ResolveRootSymlink *bool `json:"resolve_root_symlink,omitempty"`
  174. // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables.
  175. Env map[string]string `json:"env,omitempty"`
  176. resolvedDocumentRoot string
  177. preparedEnv frankenphp.PreparedEnv
  178. preparedEnvNeedsReplacement bool
  179. logger *zap.Logger
  180. }
  181. // CaddyModule returns the Caddy module information.
  182. func (FrankenPHPModule) CaddyModule() caddy.ModuleInfo {
  183. return caddy.ModuleInfo{
  184. ID: "http.handlers.php",
  185. New: func() caddy.Module { return new(FrankenPHPModule) },
  186. }
  187. }
  188. // Provision sets up the module.
  189. func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
  190. f.logger = ctx.Logger(f)
  191. if f.Root == "" {
  192. if frankenphp.EmbeddedAppPath == "" {
  193. f.Root = "{http.vars.root}"
  194. } else {
  195. rrs := false
  196. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  197. f.ResolveRootSymlink = &rrs
  198. }
  199. } else {
  200. if frankenphp.EmbeddedAppPath != "" && filepath.IsLocal(f.Root) {
  201. f.Root = filepath.Join(frankenphp.EmbeddedAppPath, f.Root)
  202. }
  203. }
  204. if len(f.SplitPath) == 0 {
  205. f.SplitPath = []string{".php"}
  206. }
  207. if f.ResolveRootSymlink == nil {
  208. rrs := true
  209. f.ResolveRootSymlink = &rrs
  210. }
  211. if !needReplacement(f.Root) {
  212. root, err := filepath.Abs(f.Root)
  213. if err != nil {
  214. return fmt.Errorf("unable to make the root path absolute: %w", err)
  215. }
  216. f.resolvedDocumentRoot = root
  217. if *f.ResolveRootSymlink {
  218. root, err := filepath.EvalSymlinks(root)
  219. if err != nil {
  220. return fmt.Errorf("unable to resolve root symlink: %w", err)
  221. }
  222. f.resolvedDocumentRoot = root
  223. }
  224. }
  225. if f.preparedEnv == nil {
  226. f.preparedEnv = frankenphp.PrepareEnv(f.Env)
  227. for e := range f.preparedEnv {
  228. if needReplacement(e) {
  229. f.preparedEnvNeedsReplacement = true
  230. break
  231. }
  232. }
  233. }
  234. return nil
  235. }
  236. // needReplacement checks if a string contains placeholdes.
  237. func needReplacement(s string) bool {
  238. return strings.Contains(s, "{") || strings.Contains(s, "}")
  239. }
  240. // ServeHTTP implements caddyhttp.MiddlewareHandler.
  241. // TODO: Expose TLS versions as env vars, as Apache's mod_ssl: https://github.com/caddyserver/caddy/blob/master/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go#L298
  242. func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
  243. origReq := r.Context().Value(caddyhttp.OriginalRequestCtxKey).(http.Request)
  244. repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
  245. var documentRootOption frankenphp.RequestOption
  246. if f.resolvedDocumentRoot == "" {
  247. documentRootOption = frankenphp.WithRequestDocumentRoot(repl.ReplaceKnown(f.Root, ""), *f.ResolveRootSymlink)
  248. } else {
  249. documentRootOption = frankenphp.WithRequestResolvedDocumentRoot(f.resolvedDocumentRoot)
  250. }
  251. env := make(map[string]string, len(f.preparedEnv)+1)
  252. env["REQUEST_URI\x00"] = origReq.URL.RequestURI()
  253. for k, v := range f.preparedEnv {
  254. if f.preparedEnvNeedsReplacement {
  255. env[k] = repl.ReplaceKnown(v, "")
  256. } else {
  257. env[k] = v
  258. }
  259. }
  260. fr, err := frankenphp.NewRequestWithContext(
  261. r,
  262. documentRootOption,
  263. frankenphp.WithRequestSplitPath(f.SplitPath),
  264. frankenphp.WithRequestPreparedEnv(env),
  265. )
  266. if err != nil {
  267. return err
  268. }
  269. return frankenphp.ServeHTTP(w, fr)
  270. }
  271. // UnmarshalCaddyfile implements caddyfile.Unmarshaler.
  272. func (f *FrankenPHPModule) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
  273. for d.Next() {
  274. for d.NextBlock(0) {
  275. switch d.Val() {
  276. case "root":
  277. if !d.NextArg() {
  278. return d.ArgErr()
  279. }
  280. f.Root = d.Val()
  281. case "split":
  282. f.SplitPath = d.RemainingArgs()
  283. if len(f.SplitPath) == 0 {
  284. return d.ArgErr()
  285. }
  286. case "env":
  287. args := d.RemainingArgs()
  288. if len(args) != 2 {
  289. return d.ArgErr()
  290. }
  291. if f.Env == nil {
  292. f.Env = make(map[string]string)
  293. f.preparedEnv = make(frankenphp.PreparedEnv)
  294. }
  295. f.Env[args[0]] = args[1]
  296. f.preparedEnv[args[0]+"\x00"] = args[1]
  297. case "resolve_root_symlink":
  298. if !d.NextArg() {
  299. continue
  300. }
  301. v, err := strconv.ParseBool(d.Val())
  302. if err != nil {
  303. return err
  304. }
  305. if d.NextArg() {
  306. return d.ArgErr()
  307. }
  308. f.ResolveRootSymlink = &v
  309. }
  310. }
  311. }
  312. return nil
  313. }
  314. // parseCaddyfile unmarshals tokens from h into a new Middleware.
  315. func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
  316. m := FrankenPHPModule{}
  317. err := m.UnmarshalCaddyfile(h.Dispenser)
  318. return m, err
  319. }
  320. // parsePhpServer parses the php_server directive, which has a similar syntax
  321. // to the php_fastcgi directive. A line such as this:
  322. //
  323. // php_server
  324. //
  325. // is equivalent to a route consisting of:
  326. //
  327. // # Add trailing slash for directory requests
  328. // @canonicalPath {
  329. // file {path}/index.php
  330. // not path */
  331. // }
  332. // redir @canonicalPath {path}/ 308
  333. //
  334. // # If the requested file does not exist, try index files
  335. // @indexFiles file {
  336. // try_files {path} {path}/index.php index.php
  337. // split_path .php
  338. // }
  339. // rewrite @indexFiles {http.matchers.file.relative}
  340. //
  341. // # FrankenPHP!
  342. // @phpFiles path *.php
  343. // php @phpFiles
  344. // file_server
  345. //
  346. // parsePhpServer is freely inspired from the php_fastgci directive of the Caddy server (Apache License 2.0, Matthew Holt and The Caddy Authors)
  347. func parsePhpServer(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error) {
  348. if !h.Next() {
  349. return nil, h.ArgErr()
  350. }
  351. // set up FrankenPHP
  352. phpsrv := FrankenPHPModule{}
  353. // set up file server
  354. fsrv := fileserver.FileServer{}
  355. disableFsrv := false
  356. // set up the set of file extensions allowed to execute PHP code
  357. extensions := []string{".php"}
  358. // set the default index file for the try_files rewrites
  359. indexFile := "index.php"
  360. // set up for explicitly overriding try_files
  361. tryFiles := []string{}
  362. // if the user specified a matcher token, use that
  363. // matcher in a route that wraps both of our routes;
  364. // either way, strip the matcher token and pass
  365. // the remaining tokens to the unmarshaler so that
  366. // we can gain the rest of the directive syntax
  367. userMatcherSet, err := h.ExtractMatcherSet()
  368. if err != nil {
  369. return nil, err
  370. }
  371. // make a new dispenser from the remaining tokens so that we
  372. // can reset the dispenser back to this point for the
  373. // php unmarshaler to read from it as well
  374. dispenser := h.NewFromNextSegment()
  375. // read the subdirectives that we allow as overrides to
  376. // the php_server shortcut
  377. // NOTE: we delete the tokens as we go so that the php
  378. // unmarshal doesn't see these subdirectives which it cannot handle
  379. for dispenser.Next() {
  380. for dispenser.NextBlock(0) {
  381. // ignore any sub-subdirectives that might
  382. // have the same name somewhere within
  383. // the php passthrough tokens
  384. if dispenser.Nesting() != 1 {
  385. continue
  386. }
  387. // parse the php_server subdirectives
  388. switch dispenser.Val() {
  389. case "root":
  390. if !dispenser.NextArg() {
  391. return nil, dispenser.ArgErr()
  392. }
  393. phpsrv.Root = dispenser.Val()
  394. fsrv.Root = phpsrv.Root
  395. dispenser.DeleteN(2)
  396. case "split":
  397. extensions = dispenser.RemainingArgs()
  398. dispenser.DeleteN(len(extensions) + 1)
  399. if len(extensions) == 0 {
  400. return nil, dispenser.ArgErr()
  401. }
  402. case "index":
  403. args := dispenser.RemainingArgs()
  404. dispenser.DeleteN(len(args) + 1)
  405. if len(args) != 1 {
  406. return nil, dispenser.ArgErr()
  407. }
  408. indexFile = args[0]
  409. case "try_files":
  410. args := dispenser.RemainingArgs()
  411. dispenser.DeleteN(len(args) + 1)
  412. if len(args) < 1 {
  413. return nil, dispenser.ArgErr()
  414. }
  415. tryFiles = args
  416. case "file_server":
  417. args := dispenser.RemainingArgs()
  418. dispenser.DeleteN(len(args) + 1)
  419. if len(args) < 1 || args[0] != "off" {
  420. return nil, dispenser.ArgErr()
  421. }
  422. disableFsrv = true
  423. }
  424. }
  425. }
  426. // reset the dispenser after we're done so that the frankenphp
  427. // unmarshaler can read it from the start
  428. dispenser.Reset()
  429. if frankenphp.EmbeddedAppPath != "" {
  430. if phpsrv.Root == "" {
  431. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, defaultDocumentRoot)
  432. fsrv.Root = phpsrv.Root
  433. rrs := false
  434. phpsrv.ResolveRootSymlink = &rrs
  435. } else if filepath.IsLocal(fsrv.Root) {
  436. phpsrv.Root = filepath.Join(frankenphp.EmbeddedAppPath, phpsrv.Root)
  437. fsrv.Root = phpsrv.Root
  438. }
  439. }
  440. // set up a route list that we'll append to
  441. routes := caddyhttp.RouteList{}
  442. // set the list of allowed path segments on which to split
  443. phpsrv.SplitPath = extensions
  444. // if the index is turned off, we skip the redirect and try_files
  445. if indexFile != "off" {
  446. // route to redirect to canonical path if index PHP file
  447. redirMatcherSet := caddy.ModuleMap{
  448. "file": h.JSON(fileserver.MatchFile{
  449. TryFiles: []string{"{http.request.uri.path}/" + indexFile},
  450. }),
  451. "not": h.JSON(caddyhttp.MatchNot{
  452. MatcherSetsRaw: []caddy.ModuleMap{
  453. {
  454. "path": h.JSON(caddyhttp.MatchPath{"*/"}),
  455. },
  456. },
  457. }),
  458. }
  459. redirHandler := caddyhttp.StaticResponse{
  460. StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
  461. Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
  462. }
  463. redirRoute := caddyhttp.Route{
  464. MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
  465. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
  466. }
  467. // if tryFiles wasn't overridden, use a reasonable default
  468. if len(tryFiles) == 0 {
  469. tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
  470. }
  471. // route to rewrite to PHP index file
  472. rewriteMatcherSet := caddy.ModuleMap{
  473. "file": h.JSON(fileserver.MatchFile{
  474. TryFiles: tryFiles,
  475. SplitPath: extensions,
  476. }),
  477. }
  478. rewriteHandler := rewrite.Rewrite{
  479. URI: "{http.matchers.file.relative}",
  480. }
  481. rewriteRoute := caddyhttp.Route{
  482. MatcherSetsRaw: []caddy.ModuleMap{rewriteMatcherSet},
  483. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
  484. }
  485. routes = append(routes, redirRoute, rewriteRoute)
  486. }
  487. // route to actually pass requests to PHP files;
  488. // match only requests that are for PHP files
  489. pathList := []string{}
  490. for _, ext := range extensions {
  491. pathList = append(pathList, "*"+ext)
  492. }
  493. phpMatcherSet := caddy.ModuleMap{
  494. "path": h.JSON(pathList),
  495. }
  496. // the rest of the config is specified by the user
  497. // using the php directive syntax
  498. dispenser.Next() // consume the directive name
  499. err = phpsrv.UnmarshalCaddyfile(dispenser)
  500. if err != nil {
  501. return nil, err
  502. }
  503. // create the PHP route which is
  504. // conditional on matching PHP files
  505. phpRoute := caddyhttp.Route{
  506. MatcherSetsRaw: []caddy.ModuleMap{phpMatcherSet},
  507. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(phpsrv, "handler", "php", nil)},
  508. }
  509. routes = append(routes, phpRoute)
  510. // create the file server route
  511. if !disableFsrv {
  512. fileRoute := caddyhttp.Route{
  513. MatcherSetsRaw: []caddy.ModuleMap{},
  514. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(fsrv, "handler", "file_server", nil)},
  515. }
  516. routes = append(routes, fileRoute)
  517. }
  518. subroute := caddyhttp.Subroute{
  519. Routes: routes,
  520. }
  521. // the user's matcher is a prerequisite for ours, so
  522. // wrap ours in a subroute and return that
  523. if userMatcherSet != nil {
  524. return []httpcaddyfile.ConfigValue{
  525. {
  526. Class: "route",
  527. Value: caddyhttp.Route{
  528. MatcherSetsRaw: []caddy.ModuleMap{userMatcherSet},
  529. HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(subroute, "handler", "subroute", nil)},
  530. },
  531. },
  532. }, nil
  533. }
  534. // otherwise, return the literal subroute instead of
  535. // individual routes, to ensure they stay together and
  536. // are treated as a single unit, without necessarily
  537. // creating an actual subroute in the output
  538. return []httpcaddyfile.ConfigValue{
  539. {
  540. Class: "route",
  541. Value: subroute,
  542. },
  543. }, nil
  544. }
  545. // Interface guards
  546. var (
  547. _ caddy.App = (*FrankenPHPApp)(nil)
  548. _ caddy.Provisioner = (*FrankenPHPModule)(nil)
  549. _ caddyhttp.MiddlewareHandler = (*FrankenPHPModule)(nil)
  550. _ caddyfile.Unmarshaler = (*FrankenPHPModule)(nil)
  551. )