initial commit, moved from system repo

This commit is contained in:
root 2020-07-30 13:09:31 -05:00
commit f923af7325
36 changed files with 3184 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/target
/classes
/checkouts
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
.hgignore
.hg/

24
CHANGELOG.md Normal file
View File

@ -0,0 +1,24 @@
# Change Log
All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/).
## [Unreleased]
### Changed
- Add a new arity to `make-widget-async` to provide a different widget shape.
## [0.1.1] - 2018-05-22
### Changed
- Documentation on how to make the widgets.
### Removed
- `make-widget-sync` - we're all async, all the time.
### Fixed
- Fixed widget maker to keep working when daylight savings switches over.
## 0.1.0 - 2018-05-22
### Added
- Files from the new template.
- Widget maker public API - `make-widget-sync`.
[Unreleased]: https://github.com/your-name/dyndns-server/compare/0.1.1...HEAD
[0.1.1]: https://github.com/your-name/dyndns-server/compare/0.1.0...0.1.1

214
LICENSE Normal file
View File

@ -0,0 +1,214 @@
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from
a Contributor if it was added to the Program by such Contributor itself or
anyone acting on such Contributor's behalf. Contributions do not include
additions to the Program which: (i) are separate modules of software
distributed in conjunction with the Program under their own license
agreement, and (ii) are not derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents" mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this
Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and
such derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under
Licensed Patents to make, use, sell, offer to sell, import and otherwise
transfer the Contribution of such Contributor, if any, in source code and
object code form. This patent license shall apply to the combination of the
Contribution and the Program if, at the time the Contribution is added by the
Contributor, such addition of the Contribution causes such combination to be
covered by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per se is
licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other
intellectual property rights of any other entity. Each Contributor disclaims
any liability to Recipient for claims brought by any other entity based on
infringement of intellectual property rights or otherwise. As a condition to
exercising the rights and licenses granted hereunder, each Recipient hereby
assumes sole responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license is required to
allow Recipient to distribute the Program, it is Recipient's responsibility
to acquire that license before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license
set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under
its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title
and non-infringement, and implied warranties or conditions of merchantability
and fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on
or through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within
the Program.
Each Contributor must identify itself as the originator of its Contribution,
if any, in a manner that reasonably allows subsequent Recipients to identify
the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a
manner which does not create potential liability for other Contributors.
Therefore, if a Contributor includes the Program in a commercial product
offering, such Contributor ("Commercial Contributor") hereby agrees to defend
and indemnify every other Contributor ("Indemnified Contributor") against any
losses, damages and costs (collectively "Losses") arising from claims,
lawsuits and other legal actions brought by a third party against the
Indemnified Contributor to the extent caused by the acts or omissions of such
Commercial Contributor in connection with its distribution of the Program in
a commercial product offering. The obligations in this section do not apply
to any claims or Losses relating to any actual or alleged intellectual
property infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim, and
b) allow the Commercial Contributor to control, and cooperate with the
Commercial Contributor in, the defense and any related settlement
negotiations. The Indemnified Contributor may participate in any such claim
at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If
that Commercial Contributor then makes performance claims, or offers
warranties related to Product X, those performance claims and warranties are
such Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a
court requires any other Contributor to pay any damages as a result, the
Commercial Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
appropriateness of using and distributing the Program and assumes all risks
associated with its exercise of rights under this Agreement , including but
not limited to the risks and costs of program errors, compliance with
applicable laws, damage to or loss of data, programs or equipment, and
unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of the
remainder of the terms of this Agreement, and without further action by the
parties hereto, such provision shall be reformed to the minimum extent
necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Program itself
(excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted
under Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and
does not cure such failure in a reasonable period of time after becoming
aware of such noncompliance. If all Recipient's rights under this Agreement
terminate, Recipient agrees to cease use and distribution of the Program as
soon as reasonably practicable. However, Recipient's obligations under this
Agreement and any licenses granted by Recipient relating to the Program shall
continue and survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to
time. No one other than the Agreement Steward has the right to modify this
Agreement. The Eclipse Foundation is the initial Agreement Steward. The
Eclipse Foundation may assign the responsibility to serve as the Agreement
Steward to a suitable separate entity. Each new version of the Agreement will
be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the
Agreement under which it was received. In addition, after a new version of
the Agreement is published, Contributor may elect to distribute the Program
(including its Contributions) under the new version. Except as expressly
stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
licenses to the intellectual property of any Contributor under this
Agreement, whether expressly, by implication, estoppel or otherwise. All
rights in the Program not expressly granted under this Agreement are
reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial
in any resulting litigation.

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# dyndns-server
A Clojure library designed to ... well, that part is up to you.
## Usage
FIXME
## License
Copyright © 2018 FIXME
Distributed under the Eclipse Public License either version 1.0 or (at
your option) any later version.

3
doc/intro.md Normal file
View File

@ -0,0 +1,3 @@
# Introduction to dyndns-server
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)

38
project.clj Normal file
View File

@ -0,0 +1,38 @@
(defproject fudo-server "0.1.0-SNAPSHOT"
:description "Fudo API Server"
:url "http://example.com/FIXME"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:main fudo.server.core
:dependencies [[org.clojure/clojure "1.9.0"]
[ring "1.6.3"]
[ring/ring-spec "0.0.4"]
[compojure "1.6.1"]
[org.flatland/protobuf "0.7.1"]
[kovacnica/clojure.network.ip "0.1.2"]
[org.clojure/data.json "0.2.6"]
[com.stuartsierra/component "0.3.2"]
[org.clojure/java.jdbc "0.7.5"]
[org.apache.commons/commons-daemon "1.0.9"]
[org.postgresql/postgresql "42.1.4"]
[io.forward/yaml "1.0.8"]
[ring/ring-json "0.4.0"]
[org.clojure/tools.cli "0.3.7"]
[base64-clj "0.1.1"]
[slingshot "0.12.2"]
[brolog "0.0.1"]
[orchestra "2017.11.12-1"]
[org.clojure/test.check "0.10.0-alpha3"]]
:aot [fudo.server.core]
:ring {:handler fudo.server.api/handler}
:plugins [[lein-protobuf "0.1.1"]
[lein-ring "0.12.4"]]
:source-paths ["src/clj" "config" "src/common" "test"]
:test-paths ["test"]
:repositories [["snapshots" {:username :env/repository_username
:password :env/repository_password
:url "https://repo.fudo.org/repository/snapshots"
:sign-releases false}]
["releases" {:username :env/repository_username
:password :env/repository_password
:url "https://repo.fudo.org/repository/internal"}]])

97
scripts/generate-keystore.rb Executable file
View File

@ -0,0 +1,97 @@
#!/usr/bin/env ruby
require "pathname"
require "tmpdir"
require "open3"
require "fileutils"
require "/fudo/lib/fudo.rb"
Fudo::require("fudo/config")
Fudo::require("network")
config = Fudo::Config::default
hostname = Fudo::Network::hostname
keystore = Pathname.new(
config.get("services::#{hostname}::service::keystore"))
passwd_file = Pathname.new(
config.get("services::#{hostname}::service::keystore_passwd_file"))
if ARGV.length != 2
puts "usage: #{$0} [KEY_FILE] [CERT_FILE]"
exit 1
end
KEYNAME = Pathname.new(ARGV[0])
CERTNAME = Pathname.new(ARGV[1])
if not File::readable?(KEYNAME)
raise RuntimeError.new("Key #{KEYNAME.to_s} can't be read!")
end
if not File::readable?(CERTNAME)
raise RuntimeError.new("Certificate #{CERTNAME.to_s} can't be read!")
end
if not keystore.parent.writable?
raise RuntimeError.new("Can't write JKS to directory #{keystore.parent.to_s}")
end
if keystore.exist?
raise RuntimeError.new("Keystore exists: #{keystore}! Aborting...")
end
if passwd_file.exist?
raise RuntimeError.new("Keystore password file exists: #{passwd_file}! Aborting...")
end
def exec_or_die(cmd)
out, err, status = Open3::capture3(cmd)
if status != 0
puts err
raise RuntimeError.new(err)
end
end
def randpass
(0...50).map { ('a'..'z').to_a[rand(26)] }.join
end
finalpass = randpass
Dir::mktmpdir { |tmp_0|
begin
PRIV = Pathname.new(tmp_0) + "private"
Dir::mkdir(PRIV)
File::chmod(0700, PRIV)
pkcs12 = PRIV + "key_crt.p12"
pem_to_pkcs12 = <<EOF
openssl pkcs12 \
-export \
-inkey #{KEYNAME.to_s} \
-in #{CERTNAME.to_s} \
-out #{pkcs12.to_s} \
-name key_crt \
-password pass:#{finalpass}
EOF
pkcs12_to_jks = <<EOF
keytool -importkeystore \
-srckeystore #{pkcs12.to_s} \
-srcstoretype pkcs12 \
-srcstorepass #{finalpass} \
-srcalias key_crt \
-destkeystore #{keystore.to_s} \
-deststoretype jks \
-deststorepass #{finalpass}
EOF
exec_or_die(pem_to_pkcs12)
exec_or_die(pkcs12_to_jks)
File::open(passwd_file, "w", 0600) { |file|
file.print(finalpass)
}
ensure
FileUtils::rm(pkcs12)
end
}

198
src/clj/fudo/api.clj Normal file
View File

@ -0,0 +1,198 @@
(ns fudo.api
(:require [clojure.spec.alpha :as s]
[clojure.spec.gen.alpha :as gen]
[clojure.string :as str]
[ring.core.protocols :as p]
[ring.util.parsing :as parse]))
(defn- lower-case? [s]
(= s (str/lower-case s)))
(defn- trimmed? [s]
(= s (str/trim s)))
(defn- char-range [a b]
(map char (range (int a) (inc (int b)))))
(def ^:private lower-case-chars
(set (char-range \a \z)))
(def ^:private alphanumeric-chars
(set (concat (char-range \A \Z) (char-range \a \z) (char-range \0 \9))))
(def ^:private uri-chars
(into alphanumeric-chars #{\- \. \_ \~ \/ \+ \,}))
(def ^:private field-name-chars
(into alphanumeric-chars #{\! \# \$ \% \& \' \* \+ \- \. \^ \_ \` \| \~}))
(def ^:private whitespace-chars
#{(char 0x09) (char 0x20)})
(def ^:private visible-chars
(set (map char (range 0x21 (inc 0x7e)))))
(def ^:private obs-text-chars
(set (map char (range 0x80 (inc 0xff)))))
(def ^:private field-value-chars*
(into whitespace-chars visible-chars))
(def ^:private field-value-chars
(into field-value-chars* obs-text-chars))
(defn- field-name-chars? [s]
(every? field-name-chars s))
(defn- field-value-chars? [s]
(every? field-value-chars s))
(defn- gen-string [chars]
(gen/fmap str/join (gen/vector (gen/elements chars))))
(defn- gen-query-string []
(->> (gen/tuple (gen/not-empty (gen/string-alphanumeric)) (gen-string uri-chars))
(gen/fmap (fn [[k v]] (str k "=" v)))
(gen/vector)
(gen/fmap #(str/join "&" %))))
(defn- gen-method []
(gen/fmap keyword (gen/not-empty (gen-string lower-case-chars))))
(defn- gen-input-stream []
(gen/fmap #(java.io.ByteArrayInputStream. %) (gen/bytes)))
(defn- gen-exception []
(gen/fmap (fn [s] (Exception. s)) (gen/string-alphanumeric)))
;; Internal
(s/def :ring.core/error
(-> #(instance? Throwable %) (s/with-gen gen-exception)))
(s/def :ring.http/field-name
(-> (s/and string? not-empty field-name-chars?)
(s/with-gen #(gen/not-empty (gen-string field-name-chars)))))
(s/def :ring.http/field-value
(-> (s/and string? field-value-chars? trimmed?)
(s/with-gen #(gen/fmap str/trim (gen-string field-value-chars*)))))
;; Request
(s/def :ring.request/server-port (s/int-in 1 65535))
(s/def :ring.request/server-name string?)
(s/def :ring.request/remote-addr string?)
(s/def :ring.request/uri
(-> (s/and string? #(str/starts-with? % "/"))
(s/with-gen (fn [] (gen/fmap #(str "/" %) (gen-string uri-chars))))))
(s/def :ring.request/query-string
(s/with-gen string? gen-query-string))
(s/def :ring.request/scheme #{:http :https})
(s/def :ring.request/request-method
(-> (s/and keyword? (comp lower-case? name))
(s/with-gen gen-method)))
(s/def :ring.request/protocol
(s/with-gen string? #(gen/return "HTTP/1.1")))
(s/def :ring.request/header-name
(-> (s/and :ring.http/field-name lower-case?)
(s/with-gen #(gen/fmap str/lower-case (s/gen :ring.http/field-name)))))
(s/def :ring.request/header-value :ring.http/field-value)
(s/def :ring.request/headers
(s/map-of :ring.request/header-name :ring.request/header-value))
(s/def :ring.request/body
(s/with-gen #(instance? java.io.InputStream %) gen-input-stream))
(s/def :ring.request/body-string string?)
(s/def ::request
(s/keys :req-un [:ring.request/server-port
:ring.request/server-name
:ring.request/remote-addr
:ring.request/uri
:ring.request/scheme
:ring.request/protocol
:ring.request/headers
:ring.request/request-method]
:opt-un [:ring.request/query-string
:ring.request/body
:ring.request/body-string]))
(s/def ::authenticated-request
(s/keys :req-un [:ring.request/server-port
:ring.request/server-name
:ring.request/remote-addr
:ring.request/uri
:ring.request/scheme
:ring.request/protocol
:ring.request/headers
:ring.request/request-method
:ring.request/request-entity]
:opt-un [:ring.request/query-string
:ring.request/body
:ring.request/body-string]))
;; Response
(s/def :ring.response/status (s/int-in 100 600))
(s/def :ring.response/header-name :ring.http/field-name)
(s/def :ring.response/header-value
(s/or :one :ring.http/field-value :many (s/coll-of :ring.http/field-value)))
(s/def :ring.response/headers
(s/map-of :ring.response/header-name :ring.response/header-value))
(s/def :ring.response/body
(-> #(satisfies? p/StreamableResponseBody %)
(s/with-gen #(gen/one-of [(gen/return nil)
(gen/string-ascii)
(gen/list (gen/string-ascii))
(gen-input-stream)]))))
(s/def ::response
(s/keys :req-un [:ring.response/status
:ring.response/headers]
:opt-un [:ring.response/body]))
;; Handler
(s/def :ring.sync.handler/args
(s/cat :request ::request))
(s/def :ring.async.handler/args
(s/cat :request ::request
:respond (s/fspec :args (s/cat :response ::response) :ret any?)
:raise (s/fspec :args (s/cat :error :ring.core/error) :ret any?)))
(s/def :ring.sync.handler/ret ::response)
(s/def :ring.async.handler/ret any?)
(s/fdef :ring.sync/handler
:args :ring.sync.handler/args
:ret :ring.sync.handler/ret)
(s/fdef :ring.async/handler
:args :ring.async.handler/args
:ret :ring.async.handler/ret)
(s/fdef :ring.sync+async/handler
:args (s/or :sync :ring.sync.handler/args :async :ring.async.handler/args)
:ret (s/or :sync :ring.sync.handler/ret :async :ring.async.handler/ret)
:fn (s/or :sync (s/keys :req-un [:ring.sync.handler/args :ring.sync.handler/ret])
:async (s/keys :req-un [:ring.async.handler/args :ring.async.handler/ret])))
(s/def ::handler
(s/or :sync :ring.sync/handler
:async :ring.async/handler
:sync+async :ring.sync+async/handler))

View File

@ -0,0 +1,29 @@
(ns fudo.api.request
(:require [fudo.api :as api]
[clojure.spec.alpha :as s]
[slingshot.slingshot :refer [throw+]]))
(defn get-header [req header]
(if-let [res (get-in req [:headers (clojure.string/lower-case (name header))])]
res
(throw+ {:type :request/header-missing
:class :client-error
:msg (format "Missing header %s in headers %s"
header (:headers req))})))
(s/fdef get-header
:args (s/cat :req ::api/request
:header string?)
:ret string?)
(defn set-header [req header value]
(when (not (string? value))
(throw+ {:type :request/invalid-header-value
:class :client-error
:msg (format "Invalid header value '%s', must be string!"
value)}))
(assoc-in req [:headers (clojure.string/lower-case (name header))] value))
(s/fdef set-header
:args (s/cat :req ::api/request
:header string?
:value string?)
:ret ::api/request)

View File

@ -0,0 +1,149 @@
(ns fudo.api.request.authenticator.auth-db
(:require [fudo.api.request.validator :as validator]
[fudo.api.request :as req]
[fudo.api :as api]
[fudo.utils :as util]
[fudo.crypto.signatures :as sig]
[fudo.db.auth-db :as auth-db]
[slingshot.slingshot :refer [throw+ try+]]
[com.stuartsierra.component :as component]
[clojure.spec.alpha :as s]))
(defn- ensure-headers [req headers]
(doseq [header headers]
(when (nil? (get-in req [:headers header]))
(throw+ {:type :validator/invalid
:class :client-error
:msg (format "Missing required header: %s" header)}))))
(defn- req-entity-name [req]
(req/get-header req "fudo-entity"))
(s/fdef req-entity-name
:args (s/cat :req ::api/request)
:ret string?)
(defn- req-timestamp [req]
(java.lang.Long/parseLong (req/get-header req "fudo-timestamp")))
(s/fdef req-timestamp
:args (s/cat :req ::api/request)
:ret integer?)
(defn- req-key-id [req]
(req/get-header req "fudo-key-id"))
(s/fdef req-key-id
:args (s/cat :req ::api/request)
:ret string?)
(defn- req-signature [req]
(req/get-header req "fudo-signature"))
(s/fdef req-signature
:args (s/cat :req ::api/request)
:ret string?)
(defn- verify-request-age [req]
(let [server-timestamp (System/currentTimeMillis)
timestamp (req-timestamp req)
lag (- server-timestamp timestamp)]
(when (> lag (* 30 1000))
(throw+ {:type :validator/invalid
:invalid-header :fudo-timestamp
:class :client-error
:msg (format "Timestamp too old (%s ms)" lag)}))
(when (< lag 0)
(throw+ {:type :validator/invalid
:class :service
:invalid-header :fudo-timestamp
:msg (format "Timestamp from the future! (%s ms)" (- lag))}))
req))
(s/fdef verify-request-age
:args (s/cat :req ::api/request)
:ret ::api/request)
(defn- verify-request-key-id [req auth-db]
(if (= (req-key-id req)
(auth-db/get-entity-key-id auth-db (req-entity-name req)))
req
(throw+ {:type :validator/invalid
:class :client-error
:invalid-header :fudo-key-id
:msg (format "Supplied key ID does not match stored entity key for %s"
(req-entity-name req))})))
(s/fdef verify-request-key-id
:args (s/cat :req ::api/request
:auth-db ::auth-db/auth-db)
:ret ::api/request)
(defn- verify-request-entity-exists [req auth-db]
(if-let [entity (auth-db/get-entity-key-id auth-db (req-entity-name req))]
req
(throw+ {:type :validator/invalid
:class :client-error
:invalid-header :fudo-entity
:msg (format "Entity does not exist: %s"
(req-entity-name req))})))
(s/fdef verify-request-entity-exists
:args (s/cat :req ::api/request
:auth-db ::auth-db/auth-db)
:ret ::api/request)
(defn- verify-request-signature [req auth-db]
(let [data (clojure.string/join [(str (req-entity-name req))
(str (req-key-id req))
(str (req-timestamp req))
(str (:body-string req))])
pubkey (auth-db/get-entity-key auth-db (req-entity-name req))]
(try+
(if (sig/verify pubkey data (req-signature req))
req
(throw+ {:type :validator/invalid
:class :client-error
:invalid-header :fudo-signature
:msg (format "Invalid signature!")}))
(catch [:type :signature/invalid] {:keys [msg]}
(throw+ {:type :validator/invalid
:class :client-error
:msg (format "Unexpected error while verifying signature: %s"
msg)})))))
(s/fdef verify-request-entity-exists
:args (s/cat :req ::api/request
:auth-db ::auth-db/auth-db)
:ret ::api/request)
(defn- authenticate-request-with-auth-db [req auth-db]
;; TODO: Alternatively, accept a cookie
(ensure-headers req ["fudo-timestamp"
"fudo-entity"
"fudo-key-id"
"fudo-signature"])
(-> req
(util/echo-through)
(verify-request-age)
(verify-request-entity-exists auth-db)
(verify-request-key-id auth-db)
(verify-request-signature auth-db)
(assoc :authenticator/authenticated true)))
(s/fdef authenticate-result-with-auth-db
:args (s/cat :req ::api/request
:auth-db ::auth-db/auth-db)
:ret ::api/request)
(defrecord AuthDbRequestAuthenticator [debug config auth-db]
component/Lifecycle
(start [this]
this)
(stop [this]
this)
validator/RequestValidator
(validate-request [this req]
(authenticate-request-with-auth-db req (:auth-db this))))
(defn init [debug]
(->AuthDbRequestAuthenticator debug nil nil))
(defn init-mock [debug config auth-db]
(->AuthDbRequestAuthenticator debug config auth-db))

View File

@ -0,0 +1,85 @@
(ns fudo.api.request.generator
(:require [fudo.api :as api]
[fudo.crypto.keys :as key]
[fudo.crypto.signatures :as sig]
[fudo.crypto.utils :as crypt]
[fudo.api.request :as req]
[clojure.spec.alpha :as s]))
(defn- input-stream? [obj]
(instance? java.io.InputStream obj))
(defn- make-body [body]
(java.io.ByteArrayInputStream. (.getBytes body)))
(s/fdef make-body
:args (s/cat :body string?)
:ret input-stream?)
(defn- generic-req []
{:server-port 1234
:server-name "test.target.fudo.org"
:remote-addr "1.2.3.4"
:scheme :https
:protocol "HTTP/1.1"
:headers {}})
(defn make-get [uri]
(assoc (generic-req)
:uri uri
:request-method :get))
(s/fdef make-get
:args (s/cat :uri string?)
:ret ::api/request)
(defn make-put [uri data]
(assoc (generic-req)
:uri uri
:request-method :put
:body-string data))
(s/fdef make-get
:args (s/cat :uri string?)
:ret ::api/request)
(defn add-host-header [req hostname]
(assoc-in req [:headers "fudo-entity"] hostname))
(s/fdef add-host-header
:args (s/cat :req ::api/request :hostname string?)
:ret ::api/request)
(defn add-fudo-secret [req secret]
(assoc-in req [:headers "fudo-secret"] secret))
(s/fdef add-fudo-secret
:args (s/cat :req ::api/request :secret string?)
:ret ::api/request)
(defn- generate-signature [req privkey]
(let [data (clojure.string/join [(req/get-header req "fudo-entity")
(req/get-header req "fudo-key-id")
(req/get-header req "fudo-timestamp")
(-> req :body-string)])]
(sig/sign privkey data)))
(s/fdef generate-signature
:args (s/cat :req ::api/request
:privkey ::key/private-key)
:ret string?)
(defn authenticate-request [req entity privkey key-id]
(let [full-req (-> req
(req/set-header "fudo-entity" entity)
(req/set-header "fudo-key-id" key-id)
(req/set-header "fudo-timestamp" (str (System/currentTimeMillis))))
sig (generate-signature full-req privkey)]
(req/set-header full-req "fudo-signature" sig)))
(s/fdef authenticate-request
:args (s/cat :req ::api/request
:entity string?
:privkey ::key/private-key
:key-id ::crypt/sha1-hash)
:ret ::api/request)
(defn remove-header [req header]
(assoc req :headers (dissoc (:headers req) header)))
(s/fdef remove-header
:args (s/cat :req ::api/request
:header string?)
:ret ::api/request)

View File

@ -0,0 +1,9 @@
(ns fudo.api.request.validator)
(defprotocol RequestValidator
(validate-request [this req]
"Given a request, perform desired validation steps. If the request
is found to be invalid, throw an exception
of :type ::fudo.api.request.validation/invalid, with a :msg
specifying the issue. Otherwise return the request, optionally
attaching additional metadata."))

View File

@ -0,0 +1,36 @@
(ns fudo.api.response)
(defn ok [body]
{:status 200
:headers {"Content-Type" "text/plain"}
:body body})
(defn created [body]
{:status 201
:headers {"Content-Type" "text/plain"}
:body body})
(defn missing [& [msg]]
{:status 404
:headers {"Content-Type" "text/plain"}
:body (str msg)})
(defn internal-error [& [msg]]
{:status 500
:headers {"Content-Type" "text/plain"}
:body (str msg)})
(defn maybe-missing [body & [missing-msg]]
(if (nil? body)
(missing missing-msg)
(ok body)))
(defn unauth [body]
{:status 401
:headers {"Content-Type" "text/plain"}
:body (str body)})
(defn client-error [& [msg]]
{:status 400
:headers {"Content-Type" "text/plain"}
:body (str msg)})

View File

@ -0,0 +1,81 @@
(ns fudo.crypto.keys
(:require [base64-clj.core :as base64]
[fudo.crypto.utils :as utils]
[clojure.spec.alpha :as s]))
(def key-algorithm "RSA")
(defn- get-key-factory []
(java.security.KeyFactory/getInstance key-algorithm))
(defn public-key? [obj]
(instance? java.security.PublicKey obj))
(s/def ::public-key public-key?)
(defn private-key? [obj]
(instance? java.security.PrivateKey obj))
(s/def ::private-key private-key?)
(s/def ::key-pair (s/keys :req [::public-key ::private-key]))
(defn generate-public-private-pair []
(let [make-pair (fn [pair]
{::private-key (.getPrivate pair)
::public-key (.getPublic pair)})]
(make-pair
(.generateKeyPair
(doto (java.security.KeyPairGenerator/getInstance key-algorithm)
(.initialize 2048))))))
(s/fdef generate-public-private-pair
:ret ::key-pair)
(defn pubkey->string [pubkey]
(-> (.. (get-key-factory)
(getKeySpec pubkey java.security.spec.X509EncodedKeySpec)
getEncoded)
base64/encode-bytes
(String. "UTF-8")))
(s/fdef pubkey->string
:args (s/cat :pubkey public-key?)
:ret string?)
(defn privkey->string [privkey]
(-> (.. (get-key-factory)
(getKeySpec privkey java.security.spec.PKCS8EncodedKeySpec)
(getEncoded))
base64/encode-bytes
(String. "UTF-8")))
(s/fdef privkey->string
:args (s/cat :privkey private-key?)
:ret string?)
(defn string->privkey [str]
(. (get-key-factory)
generatePrivate
(java.security.spec.PKCS8EncodedKeySpec.
(base64/decode-bytes (.getBytes str "UTF-8")))))
(s/fdef string->privkey
:args (s/cat :key-str string?)
:ret ::private-key)
(defn string->pubkey [str]
(. (get-key-factory)
generatePublic
(java.security.spec.X509EncodedKeySpec.
(base64/decode-bytes (.getBytes str "UTF-8")))))
(s/fdef string->pubkey
:args (s/cat :str string?)
:ret ::public-key)
(defn public-key-id [pubkey]
(utils/sha1-hash (pubkey->string pubkey)))
(s/fdef public-key-id
:args (s/cat :pubkey public-key?)
:ret utils/sha1-hash?)

View File

@ -0,0 +1,44 @@
(ns fudo.crypto.signatures
(:require [fudo.crypto.keys :as key]
[base64-clj.core :as base64]
[slingshot.slingshot :refer [throw+]]
[clojure.spec.alpha :as s]))
(def signature-algorithm "SHA256withRSA")
(defn- get-signature-factory []
(java.security.Signature/getInstance signature-algorithm))
(defn sign [privkey input]
(try
(-> (.. (doto (get-signature-factory)
(.initSign privkey)
(.update (.getBytes input "UTF-8")))
(sign))
(base64/encode-bytes)
(String. "UTF-8"))
(catch Exception e
(throw+ {:type :signature/failed
:msg (format "Failure attempting to sign input: %s"
(.getMessage e))
:cause e}))))
(s/fdef sign
:args (s/cat :privkey ::key/private-key
:input string?)
:ret string?)
(defn verify [pubkey input signature]
(try
(.. (doto (get-signature-factory)
(.initVerify pubkey)
(.update (.getBytes input "UTF-8")))
(verify (base64/decode-bytes (.getBytes signature "UTF-8"))))
(catch Exception e
(throw+ {:type :signature/invalid
:msg (format "Failure attempting to validate signature: %s"
(.getMessage e))
:cause e}))))
(s/fdef verify
:args (s/cat :pubkey ::key/public-key
:input string?
:signature string?))

View File

@ -0,0 +1,20 @@
(ns fudo.crypto.utils
(:require [base64-clj.core :as base64]
[clojure.spec.alpha :as s]))
(def hash-algorithm "sha1")
(defn sha1-hash? [str]
(and (string? str)
(= (.length str) 28)))
(s/def ::sha1-hash sha1-hash?)
(defn sha1-hash [data]
(-> (.digest (java.security.MessageDigest/getInstance hash-algorithm)
(.getBytes data "UTF-8"))
base64/encode-bytes
(String. "UTF-8")))
(s/fdef sha1-hash
:args (s/cat :data string?)
:ret ::sha1-hash)

View File

@ -0,0 +1,65 @@
(ns fudo.db.auth-db
(:require [clojure.spec.alpha :as s]
[fudo.crypto.utils :as crypt]
[fudo.crypto.keys :as key]
[fudo.net :as net]))
(defprotocol AuthDb
;; Okay, so now that we've got keys and signatures...
;;
;; - When a user registers, they generate two keys and pass their
;; pubkey to the server. It stores that to authenticate.
;;
;; - The auth header is sha1sum of the pubkey. This is used to map
;; to owned assets (eg. a dyndns hostname), as well as the
;; pubkey.
;;
;; - The body of the request should be signed with the private key,
;; and the signature should be in the 'Signature' header.
;; Headers:
;; - fudo-key-id - hash of the user/host's pubkey
;; - fudo-signature - signature of the body of the request
;; - fudo-timestamp - timestamp of request in milliseconds
;; - fudo-entity - the name of the entity who is contacting the service
;; The verify-request function:
;; - Fetches the matching key-id
;; - Checks that the signature matches the body, key-id, and timestamp
;; - Checks that the timestamp is recent
(initialize [this]
"Perform any necessary initialization steps necessary for the
database, and return an initialized instance.")
(register-entity! [this entity pubkey]
"Registers an entity by name, along with a key.")
(get-entity-key-id [this entity]
"Given an entity name, return the entity's key id.")
(get-entity-key [this entity]
"Given an entity name, return the entity's key."))
(defn auth-db? [obj]
(satisfies? AuthDb obj))
(s/def ::auth-db auth-db?)
(s/def ::entity-name net/textual-hostname?)
(s/def ::key-id crypt/sha1-hash?)
(s/def ::pubkey key/public-key?)
(s/def ::entity (s/keys :req [::entity-name ::pubkey ::key-id]))
(defn make-entity [name pubkey]
{
::entity-name name
::pubkey pubkey
::key-id (crypt/sha1-hash (key/pubkey->string pubkey))
})
(s/fdef make-entity
:args (s/cat :name ::entity-name
:pubkey ::pubkey)
:ret ::entity)

View File

@ -0,0 +1,35 @@
(ns fudo.db.dyndns-db
(:require [clojure.spec.alpha :as s]
[fudo.net.dns :as dns]))
(defprotocol DynDnsDb
(initialize [this]
"Initialize the DynDnsDb connection.")
(get-host [this host]
"Return DynDNS info related to host.")
(get-domain [this]
"Return the domain managed by this DynDnsDb instance.")
(register-v4ip! [this host v4ip]
"Set host's registered v4ip.")
(register-v6ip! [this host v6ip]
"Set host's registered v6ip.")
(register-sshfps! [this host sshfp]
"Set host's registered SSH fingerprints."))
(defrecord Host [v4ip v6ip sshfp])
(defn dyndns-db? [obj]
(satisfies? DynDnsDb obj))
(s/def ::dyndns-db dyndns-db?)
(defn to-host [v4ip v6ip sshfp]
(-> {}
(cond-> v4ip (assoc ::dns/v4ip v4ip))
(cond-> v6ip (assoc ::dns/v6ip v6ip))
(cond-> sshfp (assoc ::dns/sshfp sshfp))))
(s/fdef to-host
:args (s/cat :v4ip (s/nilable ::dns/v4ip)
:v6ip (s/nilable ::dns/v6ip)
:sshfp (s/nilable ::dns/sshfp-records))
:ret ::dns/host)

View File

@ -0,0 +1,157 @@
(ns fudo.db.pg-auth-db
(:require [fudo.db.auth-db :as auth-db]
[fudo.server.config :as config]
[fudo.crypto.utils :as crypt]
[fudo.crypto.keys :as key]
[fudo.crypto.signatures :as sig]
[fudo.server.request-validator :as validator]
[fudo.utils :as util]
[clojure.java.jdbc :as sql]
[slingshot.slingshot :refer [throw+]]
[com.stuartsierra.component :as component]
[clojure.string :refer [trim]]
[clojure.spec.alpha :as s]))
(s/def ::dbtype #{"postgresql"})
(s/def ::dbname string?)
(s/def ::user string?)
(s/def ::password string?)
(s/def ::host string?)
(s/def ::db-spec
(s/keys :req-un [::dbtype
::dbname
::user
::password
::host]))
(defn- pg-connection-spec [host dbname user passwd]
{
:dbtype "postgresql"
:dbname dbname
:user user
:password passwd
:host host
})
(s/fdef pg-connection-spec
:args (s/cat :host string?
:dbname string?
:user string?
:passwd string?)
:ret ::db-spec)
(defn- pg-connection-get-config [config]
(let [this-host (config/get-hostname)
config-path (format "services::%s::auth::auth_database"
this-host)
db-config (config/get-path config config-path)]
{
:host (config/get-env-or config
"FUDO_AUTH_DB_HOST"
(:host db-config))
:user (config/get-env-or config
"FUDO_AUTH_DB_USER"
(:user db-config))
:database (config/get-env-or config
"FUDO_AUTH_DB_DATABASE"
(:database db-config))
:passwd (config/get-env-or config
"FUDO_AUTH_DB_PASSWD"
(slurp (:passwd_file db-config)))
}))
(defn- pg-connect [cfg]
(pg-connection-spec (:host cfg) (:database cfg) (:user cfg) (:passwd cfg)))
(defn- row->entity [row]
{
::auth-db/entity-name (trim (:entity_name row))
::auth-db/key-id (trim (:key_id row))
::auth-db/pubkey (key/string->pubkey (trim (:public_key row)))
})
(s/fdef row->entity
:args (s/cat :row (s/map-of simple-keyword? any?))
:ret ::auth-db/entity)
(defn- get-by-name [db-spec name]
(first
(map row->entity
(sql/query db-spec
["SELECT entity_name, key_id, public_key FROM entities WHERE entity_name = ?"
name]))))
(s/fdef get-by-name
:args (s/cat :db-spec ::db-spec
:name ::auth-db/entity-name)
:ret ::auth-db/entity)
(defn- insert-entity! [db-spec data]
(sql/insert! db-spec
:entities
{:entity_name (::auth-db/entity-name data)
:key_id (::auth-db/key-id data)
:public_key (key/pubkey->string (::auth-db/pubkey data))})
(::auth-db/key-id data))
(s/fdef insert-entity!
:args (s/cat :db-spec ::db-spec
:data ::auth-db/entity)
:ret ::auth-db/key-id)
(defn- get-entity-key [db-spec name]
(::auth-db/pubkey (get-by-name db-spec name)))
(s/fdef get-entity-key
:args (s/cat :db-spec ::db-spec
:name ::auth-db/name)
ret ::auth-db/pubkey)
(defn- initialize-connection [config]
(pg-connect (pg-connection-get-config config)))
(s/def ::debug boolean?)
(s/def ::initialized-pg-auth-db
(s/keys :req-un [::debug ::config/config ::db-spec]))
(s/def ::uninitialized-pg-auth-db
(s/keys :req-un [::debug]))
(defrecord PGAuthDb [debug config db-spec]
component/Lifecycle
(start [this]
(->PGAuthDb (:debug this)
(:config this)
(initialize-connection (:config this))))
(stop [this] this)
auth-db/AuthDb
validator/RequestValidator
(initialize [this]
(->PGAuthDb (:debug this)
(:config this)
(initialize-connection (:config this))))
(register-entity! [this entity-name pubkey]
(let [key-id (insert-entity! (:db-spec this)
(auth-db/make-entity entity-name pubkey))]
(util/info (format "Inserted new key %s for entity %s" key-id entity-name))
{::auth-db/key-id key-id}))
(get-entity-key-id [this entity]
(some-> (:db-spec this)
(get-by-name entity)
::auth-db/key-id))
(get-entity-key [this entity]
(some-> (:db-spec this)
(get-by-name entity)
::auth-db/pubkey)))
(defn init [debug]
(->PGAuthDb debug nil nil))
(s/fdef init
:args (s/cat :debug boolean?)
:ret ::uninitialized-pg-auth-db)

View File

@ -0,0 +1,402 @@
(ns fudo.db.pg-dyndns-db
(:require [fudo.db.dyndns-db :as dyndns-db]
[fudo.utils :as util]
[fudo.server.config :as config]
[fudo.net.ip :as ip]
[fudo.net :as net]
[fudo.net.dns :as dns]
[clojure.java.jdbc :as sql]
[clojure.set :as set]
[com.stuartsierra.component :as component]
[clojure.string :as str]
[clojure.spec.alpha :as s]))
(s/def ::dbtype #{"postgresql"})
(s/def ::dbname string?)
(s/def ::user string?)
(s/def ::password string?)
(s/def ::host string?)
(s/def ::db-spec
(s/keys :req-un [::dbtype
::dbname
::user
::password
::host]))
(defn- pg-connection-spec [host dbname user passwd]
{
:dbtype "postgresql"
:dbname dbname
:user user
:password passwd
:host host
})
(s/fdef pg-connection-spec
:args (s/cat :host string?
:dbname string?
:user string?
:password string?)
:ret ::db-spec)
(defn- get-config-domain [config]
(let [this-host (config/get-hostname)]
(config/get-path config (format "services::%s::dyndns::domain"
this-host))))
(s/fdef get-config-domain
:args (s/cat :config ::config/config)
:ret ::net/domain)
(defn- make-fqdn [host domain]
(format "%s.%s" host domain))
(s/fdef make-fqdn
:args (s/cat :host string? :domain string?)
:ret ::net/host-fqdn
:fn (s/and #(str/starts-with? (:ret %) (-> % :args :host))
#(str/ends-with? (:ret %) (-> % :args :domain))))
(defn- take-hostname [fqdn domain]
(let [domain-regex (re-pattern (format ".%s$" domain))]
(str/replace-first fqdn domain-regex "")))
(s/fdef take-hostname
:args (s/cat :fqdn string? :domain string?)
:ret string?
:fn #(str/starts-with? (-> % :args :fqdn) (:ret %)))
(defn positive-integer? [val]
(and (integer? val) (> val 0)))
(s/def ::id positive-integer?)
(s/def ::domain-id positive-integer?)
(s/def ::type #{:SOA :SSHFP :A :AAAA :CNAME :NS :PTR :TXT :MX :SPF :RP :SRV})
(s/def ::name net/textual-hostname?)
(s/def ::content string?)
(s/def ::record (s/cat :type ::type :name ::name :content ::content))
(defmulti valid-record first)
(defmethod valid-record :SOA [_]
(s/cat :type #{:SOA}
:name ::net/host-fqdn
:content ::valid-soa-record))
(defmethod valid-record :SSHFP [_]
(s/cat :type #{:SSHFP}
:name ::net/host-fqdn
:content ::dns/sshfp-record))
(defmethod valid-record :A [_]
(s/cat :type #{:A}
:name ::net/host-fqdn
:content ::ip/v4ip))
(defmethod valid-record :AAAA [_]
(s/cat :type #{:AAAA}
:name ::net/host-fqdn
:content ::ip/v6ip))
(defmethod valid-record :CNAME [_]
(s/cat :type #{:CNAME}
:name ::net/host-fqdn
:content ::net/host-fqdn))
(defmethod valid-record :NS [_]
(s/cat :type #{:NS}
:name ::net/host-fqdn
:content ::net/host-fqdn))
(defmethod valid-record :PTR [_]
(s/cat :type #{:PTR}
:name ::net/host-fqdn
:content ::valid-ptr-record))
(defmethod valid-record :TXT [_]
(s/cat :type #{:TXT}
:name ::net/host-fqdn
:content string?))
(defmethod valid-record :MX [_]
(s/cat :type #{:MX}
:name ::net/host-fqdn
:content ::valid-mx-record))
(defmethod valid-record :RP [_]
(s/cat :type #{:RP}
:name ::net/host-fqdn
:content ::net/host-fqdn))
(defmethod valid-record :SRV [_]
(s/cat :type #{:SRV}
:name :net/srv-name
:content ::valid-srv-record))
(defn valid-mx-record? [rec]
(and (string? rec)
(let [[pref host] (str/split rec #" ")]
(and (not (nil? (re-matches #"^[0-9]{1,2}$" pref)))
(net/textual-hostname? host)))))
(s/def ::valid-mx-record valid-mx-record?)
(defn valid-ptr-record? [rec]
(let [v6-rx #"^([0-9a-f]\.){32}ip6.arpa\.?$"
v4-rx #"^(([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){4}in-addr\.arpa\.?$"]
(and (string? rec)
(or (not (nil? (re-matches v6-rx rec)))
(not (nil? (re-matches v4-rx rec)))))))
(s/def ::valid-ptr-record valid-ptr-record?)
(defn valid-soa-record? [rec]
(and (string? rec)
(let [[ns-host id-host] (str/split rec #" ")]
(and (net/textual-hostname? ns-host)
(net/textual-hostname? id-host)))))
(s/def ::valid-soa-record valid-soa-record?)
(defn valid-srv-record? [rec]
(and (string? rec)
(let [[share-str host] (str/split rec #" ")
share (Integer. share-str)]
(and (> share 0)
(<= share 100)
(net/textual-hostname? host)))))
(s/def ::valid-srv-record valid-srv-record?)
(s/def ::resource-record
(s/keys :req [::id ::domain-id ::name ::type ::content]))
(defn- row->resource-record [rec]
{
::id (-> rec :id)
::domain-id (-> rec :domain_id)
::name (-> rec :name)
::type (-> rec :type keyword)
::content (-> rec :content)
})
(s/fdef row->resource-record
:args (s/cat :rec (s/map-of simple-keyword? any?))
:ret ::resource-record)
(defn- get-host-records [db-spec domain-id hostname]
(let [qry ["SELECT * FROM records WHERE domain_id = ? AND name = ?"
domain-id
hostname]]
(clojure.pprint/pprint qry)
(map row->resource-record
(sql/query db-spec
qry))))
(s/fdef get-host-records
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn)
:ret ::dns/host)
(defn- pg-get-host [db-spec domain-id hostname]
(let [find-first (fn [fn lst] (first (filter fn lst)))
records (get-host-records db-spec domain-id hostname)
v4ip-rec (find-first #(= (::type %) :A) records)
v6ip-rec (find-first #(= (::type %) :AAAA) records)
sshfp-recs (filter #(= (::type %) :SSHFP) records)]
(dyndns-db/to-host (some-> v4ip-rec
::content
ip/address)
(some-> v6ip-rec
::content
ip/address)
(map :content sshfp-recs))))
(s/fdef pg-get-host
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn)
:ret ::dns/host)
(defn- update-record-content-by-id! [db-spec record-id content]
(sql/update! db-spec
:records
{:content content}
["id = ?" record-id])
true)
(s/fdef update-record-content-by-id!
:args (s/cat :db-spec ::db-spec
:record-id ::id
:content string?)
:ret boolean?)
(defn- delete-sshfp-records! [db-spec domain-id hostname]
(sql/delete! db-spec
:records
["name = ? AND domain_id = ? AND type = 'SSHFP'"
hostname
domain-id])
true)
(s/fdef delete-sshfp-records!
:args (s/cat :db-spec ::db-spec :domain-id ::domain-id :hostname ::net/host-fqdn)
:ret boolean?)
(defn- insert-record-by-type! [db-spec domain-id hostname type content]
(sql/insert! db-spec
:records
{:name hostname
:type (name type)
:content content
:domain_id domain-id})
true)
(s/fdef insert-record-by-type!
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:type ::type
:content string?)
:ret boolean?)
(defn- get-record-by-type [db-spec domain-id hostname type]
(some-> (sql/query db-spec
["SELECT * FROM records WHERE name = ? AND type = ? AND domain_id = ?"
hostname
(name type)
domain-id])
first
row->resource-record))
(s/fdef get-record-by-type
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:type ::type)
:ret (s/nilable ::resource-record))
(defn- maybe-update-host-content! [db-spec domain-id hostname rec-type content]
(if-let [rec (get-record-by-type db-spec domain-id hostname rec-type)]
(update-record-content-by-id! db-spec (::id rec) content)
(insert-record-by-type! db-spec domain-id hostname rec-type content)))
(s/fdef maybe-update-host-content!
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:type ::type
:content string?)
:ret boolean?)
(defn- pg-register-v4ip! [db-spec domain-id hostname v4ip]
(maybe-update-host-content! db-spec domain-id hostname :A (ip/to-string v4ip)))
(s/fdef pg-register-v4ip!
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:v4ip ::ip/v4ip)
:ret boolean?)
(defn- pg-register-v6ip! [db-spec domain-id hostname v6ip]
(maybe-update-host-content! db-spec domain-id hostname :AAAA (ip/to-string v6ip)))
(s/fdef pg-register-v6ip!
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:v6ip ::ip/v6ip)
:ret boolean?)
(defn- pg-register-sshfp! [db-spec domain-id hostname sshfps]
(do (delete-sshfp-records! db-spec domain-id hostname)
(doseq [sshfp sshfps]
(insert-record-by-type! db-spec domain-id hostname :SSHFP sshfp))))
(s/fdef pg-register-sshfp!
:args (s/cat :db-spec ::db-spec
:domain-id ::domain-id
:hostname ::net/host-fqdn
:sshfps ::dns/sshfp-records)
:ret boolean?)
(defn- pg-get-domain-id [db-spec domain]
(some-> (sql/query db-spec
["SELECT id FROM domains WHERE name = ? AND type = 'MASTER'"
domain])
first
:id))
(s/fdef pg-get-domain-id
:args (s/cat :db-spec ::db-spec
:domain ::net/domain)
:ret (s/nilable ::domain-id))
(defn- get-sshfp [db-spec host]
(some-> (sql/query db-spec
["SELECT content FROM domains WHERE name = "])))
(s/fdef get-sshfp
:args (s/cat :db-spec ::db-spec
:host ::net/host-fqdn)
:ret ::dns/sshfp-records)
(defn- initialize-connection [config]
(let [this-host (config/get-hostname)
db-config-path (format "services::%s::dyndns::dns_database"
this-host)
db-config (config/get-path config db-config-path)]
(pg-connection-spec (config/get-env-or config
"FUDO_DYNDNS_DB_HOST"
(:host db-config))
(config/get-env-or config
"FUDO_DYNDNS_DB_DATABASE"
(:database db-config))
(config/get-env-or config
"FUDO_DYNDNS_DB_USER"
(:user db-config))
(config/get-env-or config
"FUDO_DYNDNS_DB_PASSWD"
(slurp (:passwd_file db-config))))))
(defn- pg-sshfp-changed? [db-spec host domain-id sshfps]
(let [db-sshfps (::dyndns-db/sshfp (pg-get-host db-spec domain-id host))]
(not (empty? (set/difference sshfps db-sshfps)))))
(s/fdef pg-sshfp-changed?
:args (s/cat :db-spec ::db-spec
:host ::net/host-fqdn
:domain-id ::domain-id
:sshfps ::dns/sshfp-records))
(defrecord PGDynDnsDb [debug config conn domain]
dyndns-db/DynDnsDb
(initialize [this]
(->PGDynDnsDb (:debug this debug)
(:config this)
(initialize-connection config)
(get-config-domain config)))
(get-host [this host]
(pg-get-host (:conn this)
(pg-get-domain-id (:conn this) (:domain this))
(make-fqdn host (:domain this))))
(register-v4ip! [this host v4ip]
(let [result
(pg-register-v4ip! (:conn this)
(pg-get-domain-id (:conn this) (:domain this))
(make-fqdn host (:domain this))
v4ip)]
(util/info (format "Registered v4ip %s for host %s."
(ip/to-string v4ip) host))
result))
(register-v6ip! [this host v6ip]
(let [result
(pg-register-v6ip! (:conn this)
(pg-get-domain-id (:conn this) (:domain this))
(make-fqdn host (:domain this))
v6ip)]
(util/info (format "Registered v6ip %s for host %s."
(ip/to-string v6ip) host))
result))
(register-sshfps! [this host sshfps]
(if (pg-sshfp-changed? (:conn this)
host
(pg-get-domain-id (:conn this) (:domain this))
sshfps)
(do (println "SSHFP changed! Inserting!")
(let [result
(pg-register-sshfp! (:conn this)
(pg-get-domain-id (:conn this) (:domain this))
(make-fqdn host (:domain this))
sshfps)]
(util/info (format "Registered new SSHFP records for host %s"
host))
result))
(do (println "SSHFP unchanged!") true)))
(get-domain [this]
(get-config-domain (:config this))))
(defn init [debug config]
(->PGDynDnsDb debug config nil nil))
(s/fdef create
:args (s/cat :debug boolean?
:config ::config/config)
:ret ::dyndns-db/dyndns-db)

View File

@ -0,0 +1,36 @@
(ns fudo.db.transient-auth-db
(:require [fudo.db.auth-db :as auth-db]
[fudo.crypto.keys :as key]
[fudo.crypto.utils :as crypt]
[clojure.spec.alpha :as s]
[com.stuartsierra.component :as component]))
(defrecord TransientAuthDb [debug config keystore]
component/Lifecycle
(start [this] this)
(stop [this] this)
auth-db/AuthDb
(initialize [this] this)
(register-entity! [this entity-name pubkey]
(let [entity (auth-db/make-entity entity-name pubkey)]
(swap! (:keystore this)
(fn [store]
(assoc store entity-name entity)))
(::auth-db/key-id entity)))
(get-entity-key-id [this entity]
(some-> (get @keystore entity)
::auth-db/key-id))
(get-entity-key [this entity]
(some-> (get @keystore entity)
::auth-db/pubkey)))
(defn create [debug config auth-hosts]
(->TransientAuthDb debug config (atom auth-hosts)))

View File

@ -0,0 +1,55 @@
(ns fudo.db.transient-dyndns-db
(:require [fudo.db.dyndns-db :as dyndns-db]
[fudo.net.dns :as dns]
[fudo.net.ip :as ip]
[fudo.net :as net]
[clojure.spec.alpha :as s]
[com.stuartsierra.component :as component]))
(s/def ::hostmap (s/map-of string? ::dns/host))
(defn pp [obj]
(clojure.pprint/pprint obj)
obj)
(defrecord TransientDynDnsDb [debug config domain store]
dyndns-db/DynDnsDb
(initialize [this] this)
(get-host [this hostname]
(let [host (get @(:store this) hostname)]
(dyndns-db/to-host (some-> host ::dns/v4ip)
(some-> host ::dns/v6ip)
(some-> host ::dns/sshfp))))
(register-v4ip! [this host v4ip]
(swap! (:store this)
(fn [old-store]
(assoc-in old-store [host :v4ip] v4ip))))
(register-v6ip! [this host v6ip]
(swap! (:store this)
(fn [old-store]
(assoc-in old-store [host :v6ip] v6ip))))
(register-sshfps! [this host sshfps]
(swap! (:store this)
(fn [old-store]
(assoc-in old-store [host :sshfps] sshfps))))
(get-domain [this]
(:domain this)))
(defn create [debug config domain hostmap]
(->TransientDynDnsDb debug
config
domain
(atom hostmap)))
(s/fdef create
:args (s/cat :debug boolean?
:config any?
:domain ::net/domain
:hostmap ::hostmap)
:ret ::dyndns-db/dyndns-db)

34
src/clj/fudo/net.clj Normal file
View File

@ -0,0 +1,34 @@
(ns fudo.net
(:require [clojure.spec.alpha :as s]))
(def host-element "([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])")
(defn textual-hostname? [str]
(let [host-rx (re-pattern (format "^(%s\\.)*%s\\.?$" host-element host-element))]
(not (nil? (re-matches host-rx str)))))
(defn host-fqdn? [str]
"A hosts' FQDN must contain at least three components,
since (realistically) any two-element hostname (eg. myhost.org) must
be a mistake in the context of dyndns."
(let [fqdn-rx (re-pattern (format "^(%s\\.){2,}%s\\.?$" host-element host-element))]
(not (nil? (re-matches fqdn-rx str)))))
(defn domain? [str]
(let [domain-rx (re-pattern (format "^(%s\\.)*%s\\.?$" host-element host-element))]
(not (nil? (re-matches domain-rx str)))))
(defn srv-name? [str]
(let [srv-name-rx (re-pattern (format "^_%s\\._(tcp|udp)\\.(%s\\.){1,}%s\\.?$"
host-element
host-element
host-element))]
(not (nil? (re-matches srv-name-rx str)))))
(s/def ::hostname textual-hostname?)
(s/def ::host-fqdn host-fqdn?)
(s/def ::domain domain?)
(s/def ::srv-name srv-name?)

19
src/clj/fudo/net/dns.clj Normal file
View File

@ -0,0 +1,19 @@
(ns fudo.net.dns
(:require [clojure.spec.alpha :as s]
[fudo.net.ip :as ip]))
(defn sshfp-record? [rec]
(let [sshfp-rx #"^[0-5] [0-5] [0-9A-Za-z]{32,128}$"]
(and (string? rec)
(not (nil? (re-matches sshfp-rx rec))))))
(s/def ::sshfp-record sshfp-record?)
(s/def ::v4ip (s/nilable ip/v4?))
(s/def ::v6ip (s/nilable ip/v6?))
(s/def ::sshfp-records (s/coll-of ::sshfp-record))
(s/def ::host
(s/keys :opt [::v4ip ::v6ip ::sshfp-records]))

59
src/clj/fudo/net/ip.clj Normal file
View File

@ -0,0 +1,59 @@
(ns fudo.net.ip
(:require [slingshot.slingshot :refer [try+ throw+]]
[clojure.spec.alpha :as s]))
(defn v6? [addr]
(instance? java.net.Inet6Address addr))
(defn v4? [addr]
(instance? java.net.Inet4Address addr))
(defn address? [addr]
(or (v6? addr) (v4? addr)))
(defn address [addr-str]
(try
(java.net.InetAddress/getByName addr-str)
(catch java.net.UnknownHostException ex
(throw+ {:type :ip/invalid-ip-address
:class :client-error
:address-string addr-str
:msg (format "Invalid IP address: %s" addr-str)}))))
(defn valid-ip? [addr-string]
(try+
(address addr-string)
true
(catch [:type ::invalid-ip-address] _
false)))
(defn v4ip? [str]
(and (valid-ip? str)
(v4? (address str))))
(defn v6ip? [str]
(and (valid-ip? str)
(v6? (address str))))
(s/def ::v4ip v4?)
(s/def ::v6ip v6?)
(s/fdef address
:args (s/cat :addr-str string?)
:ret address?)
(s/def ::v4ip-str v4ip?)
(s/def ::v6ip-str v6ip?)
(defn local? [addr]
(or (.isAnyLocalAddress addr)
(.isLoopbackAddress addr)
(.isSiteLocalAddress addr)))
(defn global? [addr]
(not (local? addr)))
(defn to-string [addr]
(.getHostAddress addr))
(s/fdef to-string
:args (s/cat :addr address?)
:ret string?)

62
src/clj/fudo/server.clj Normal file
View File

@ -0,0 +1,62 @@
(ns fudo.server
(:require [fudo.server.config :as config]
[fudo.db.auth-db :as auth-db]
[fudo.server.service :as service]
[fudo.server.services :as services]
[fudo.server.middleware :as middleware]
[ring.adapter.jetty :refer [run-jetty]]
[slingshot.slingshot :refer [try+ throw+]]
[com.stuartsierra.component :as component]
[clojure.spec.alpha :as s]))
(defn- get-file-contents [filename]
(when (not (.exists (clojure.java.io/as-file filename)))
(throw+ {:type :server/file-error
:class :server-error
:msg (format "File doesn't exist: %s"
filename)}))
(clojure.string/trim (slurp filename)))
(s/fdef get-file-contents
:args (s/cat :filename string?)
:ret (s/nilable string?))
(defn start-server [debug config fudo-services]
(let [api-handler (service/generate-handler fudo-services)
this-host (config/get-hostname)
service-config (config/get-path config (format "services::%s::service" this-host))
server-port (config/get-env-or config
"FUDO_SERVICE_PORT"
(:port service-config))
keystore (config/get-env-or config
"FUDO_SERVER_KEYSTORE"
(:keystore service-config))
key-password (config/get-env-or config
"FUDO_SERVER_KEYSTORE_PASSWD"
(get-file-contents
(:keystore_passwd_file service-config)))]
(println (format "Starting server on port %s" server-port))
(run-jetty api-handler
{:ssl-port server-port
:http? false
:ssl? true
:keystore keystore
:key-password key-password})))
(defrecord FudoServer [debug config fudo-services server]
component/Lifecycle
(start [this]
(->FudoServer (:debug this)
(:config this)
(:fudo-services this)
(start-server (:debug this)
(:config this)
(:fudo-services this))))
(stop [this]
(.stop (:server this))))
(defn init [debug]
(->FudoServer debug nil nil nil))

View File

@ -0,0 +1,184 @@
(ns fudo.server.config
(:require [clojure.string :as str]
[clojure.data.json :as json]
[yaml.core :as yaml]
[slingshot.slingshot :refer [throw+]]
[com.stuartsierra.component :as component]
[clojure.spec.alpha :as s]))
(def DELIM #"::")
(defn- load-env-config [prefix]
(into {}
(map (fn [key]
{(keyword key) (System/getenv key)}))
(filter #(str/starts-with? % prefix)
(keys (System/getenv)))))
(defn- load-yaml-file [file] (yaml/from-file file))
(defn- load-json-file [file] (json/read-str (slurp file)
:key-fn keyword))
(defn- yaml-file? [file] (str/ends-with? (.getName file) ".yml"))
(defn- json-file? [file] (str/ends-with? (.getName file) ".json"))
(defn- make-key [file]
(if (.isDirectory file)
(keyword (.getName file))
(keyword
(str/join "." (-> (.getName file)
(str/split #"\.")
butlast)))))
(defn- load-file-config [path-file]
(when (not (.isDirectory path-file))
(throw+ {:type :config/invalid-filesystem-path
:path path-file
:class :server-error
:msg (format "Not a valid configuration path: %s"
(.getPath path-file))}))
(let [path-contents (.listFiles path-file)]
(into {}
(map (fn [file]
(cond (.isDirectory file) {(make-key file)
(load-file-config file)}
(json-file? file) {(make-key file)
(load-json-file file)}
(yaml-file? file) {(make-key file)
(load-yaml-file file)}
:else {})))
path-contents)))
(defn- print-nested-config [config depth]
(defn- make-space [this-depth]
(str/join (map (fn [_] " ") (range this-depth))))
(defn- print-el [el this-depth]
(cond (map? el) (print-nested-config el this-depth)
(sequential? el) (do
(println (format "%s>>" (make-space this-depth)))
(doseq [e0 el] (print-el e0 (+ this-depth 2))))
:else (println (format "%s%s"
(make-space this-depth)
el))))
(doseq [key (keys config)]
(println (format "%s%s::" (make-space depth) (name key)))
(print-el (get config key) (+ depth 2))))
(defn- print-config-and-env [config]
(println "ENV")
(let [env (:env config)]
(doseq [key (keys env)]
(println (format " %s: %s" key (get env key)))))
(println "CONFIG")
(print-nested-config (:config config) 2))
(defprotocol FudoConfig
"A protocol for reading configurations."
(get-path [self path])
(get-env [self env-var])
(get-env-or [self env-var else])
(print-config [self])
(refresh [self])
(has-path? [self path]))
(defrecord FudoServerConfig [debug config-path env-prefix config env]
component/Lifecycle
(start [self]
(when debug (print "Loading server config..."))
(let [config (->FudoServerConfig debug
(:config-path self)
(:env-prefix self)
(load-file-config (:config-path self))
(load-env-config (:env-prefix self)))]
(when debug (println "done."))
config))
(stop [self]
(when debug (println "Dropping server config...done."))
self)
FudoConfig
(get-path [self path]
(let [path-els (map keyword (str/split path DELIM))]
(if-let [value (get-in (:config self) path-els)]
value
(throw+ {:type ::invalid-path
:path path
:class :server-error
:msg (format "Invalid config path: %s" path)}))))
(get-env [self env-var]
(get (:env self) (keyword env-var)))
(get-env-or [self env-var else]
(if-let [val (get-env self env-var)]
val
else))
(print-config [self]
(print-config-and-env self))
(refresh [self]
(when debug (print "Reloading server config..."))
(->FudoServerConfig debug
(:config-path self)
(:env-prefix self)
(load-file-config (:config-path self))
(load-env-config (:env-prefix self)))
(when debug (println "done.")))
(has-path? [self path]
(let [path-els (map keyword (str/split path DELIM))]
(not (nil? (get-in (:config self) path-els)))))
Object
(toString [_] "<FudoConfig>"))
(defrecord MockConfig [env]
FudoConfig
(get-path [self path]
(throw+ {:type :mock-not-implemented
:class :mock-error}))
(get-env [self env-var]
(get (:env self) env-var))
(get-env-or [self env-var else]
(throw+ {:type :mock-not-implemented
:class :mock-error}))
(print-config [self]
(throw+ {:type :mock-not-implemented
:class :mock-error}))
(refresh [self]
(throw+ {:type :mock-not-implemented
:class :mock-error}))
(has-path? [self path]
(throw+ {:type :mock-not-implemented
:class :mock-error})))
(defn config? [obj]
(satisfies? FudoConfig obj))
(s/def ::config config?)
(defn init-config [debug config-path env-prefix]
(->FudoServerConfig debug (clojure.java.io/file config-path) env-prefix nil nil))
(defn init-mock-config [env]
(->MockConfig env))
(defn get-fqdn []
(.getHostName (java.net.InetAddress/getLocalHost)))
(defn get-hostname []
(first (str/split (get-fqdn) #"\.")))

View File

@ -0,0 +1,121 @@
(ns fudo.server.core
(:import [org.apache.commons.daemon Daemon DaemonContext])
(:gen-class :implements [org.apache.commons.daemon.Daemon])
(:require [com.stuartsierra.component :as component]
[clojure.tools.cli :refer [parse-opts]]
[clojure.string :as str]
[fudo.server.config :as config]
[fudo.server.service :as service]
[fudo.server.services :as services]
[fudo.server.services.dyndns :as srv:dyndns]
[fudo.server.services.auth :as srv:auth]
[fudo.api.request.authenticator.auth-db :as authenticator]
[fudo.db.pg-auth-db :as pg-auth-db]
[fudo.db.dyndns-db :as dyndns-db]
[fudo.db.pg-dyndns-db :as pg-dyndns-db]
[fudo.server :as server]
[brolog.logging :as log]
[clojure.spec.alpha :as s]
[orchestra.spec.test :as spectest]))
(spectest/instrument)
;; Eventually, look at configs and shit. For now, hardcode.
(defn- initialize-service-manifest [debug config auth-db]
(let [dyndns-db (pg-dyndns-db/init debug config)]
[(srv:dyndns/create debug config (dyndns-db/initialize dyndns-db))
(srv:auth/create debug config auth-db)]))
(s/fdef create-local-services
:args (s/cat :config ::config/config)
:ret (s/coll-of ::service/service))
(defrecord LocalServices [debug config auth-db services]
component/Lifecycle
(start [this]
(->LocalServices (:debug this)
(:config this)
(:auth-db this)
(initialize-service-manifest (:debug this)
(:config this)
(:auth-db this))))
(stop [this] this))
(defn- create-service-manifest [debug]
(->LocalServices debug nil nil nil))
(defn- make-service-system [debug config-path env-prefix]
(component/system-map
:config (config/init-config debug config-path env-prefix)
:auth-db (component/using (pg-auth-db/init debug)
[:config])
:authenticator (component/using (authenticator/init debug)
[:config :auth-db])
:local-services (component/using (create-service-manifest debug)
[:config :auth-db])
:fudo-services (component/using (services/init debug)
[:config :local-services :authenticator])
:fudo-server (component/using (server/init debug)
[:config :fudo-services])))
(def ^:dynamic *server* (atom nil))
(defn- directory? [filename]
(.isDirectory (clojure.java.io/file filename)))
(defn- load-args [args]
(let [server-options
[["-c" "--config-path PATH"
"Path from which to load configuration."
:default "/fudo/config"
:validate [directory? "Config must point to a valid filesystem path."]]
["-e" "--env-prefix PREFIX"
"Prefix of environment variables to be captured as configuration."
:default "FUDO"]
["-d" "--debug"
"Set the debug flag to request more verbose output."]]
args (parse-opts args server-options)]
(when (:errors args)
(throw (java.lang.IllegalArgumentException.
(format "Failure parsing args %s: \n%s"
(str/join " " args)
(str/join "\n" (:errors args))))))
(:options args)))
(defn- init [args]
(println "Initializing Fudo Server")
(log/init-logging "fudo-service" brolog.constants/log-local3)
(let [args (load-args args)
debug (or (:debug args)
(not (nil? (System/getenv "DEBUG"))))]
(swap! *server*
(fn [_]
(make-service-system debug
(:config-path args)
(:env-prefix args))))))
(defn- start []
(println "Starting Fudo Server")
(swap! *server*
(fn [system]
(component/start system))))
(defn- stop []
(swap! *server*
(fn [system]
(component/stop system))))
(defn -init [this ^DaemonContext context]
(init (.getArguments context)))
(defn -start [this]
(future (start)))
(defn -stop [this]
(stop))
(defn -main [& args]
(init args)
(start))

View File

@ -0,0 +1,169 @@
(ns fudo.server.middleware
(:require [fudo.db.auth-db :as auth-db]
[fudo.api.request.validator :as validator]
[fudo.api.response :as response]
[fudo.utils :as util]
[ring.util.request :as request]
[slingshot.slingshot :refer [try+ throw+]]
[clojure.data.json :as json]))
(defn validation-middleware [validator handler]
(fn
([request]
(try+
(handler
(validator/validate-request validator request))
(catch [:type :validator/invalid] {:keys [msg]}
{:status 400
:headers {}
:body msg})))
([request respond raise]
(handler
(try+
(validator/validate-request validator request)
(catch [:type :validator/invalid] {:keys [msg]}
(respond {:status 400
:headers {}
:body msg})))
respond
raise))))
(defn- print-data [debug title data]
(when debug
(println title)
(clojure.pprint/pprint data))
data)
(defn- pp [obj]
(clojure.pprint/pprint obj)
obj)
(defn echo-middleware [debug handler]
(fn
([request] (print-data debug "RESPONSE:"
(handler (print-data debug "REQUEST:" request))))
([request respond raise]
(handler (print-data debug "REQUEST:" request)
#(respond (print-data debug "RESPONSE:" %))
raise))))
(defn json-wrapper [handler]
(let [empty->nil (fn [input] (if (empty? input) nil input))
decode-body (fn [req]
(try+
(assoc req :body-string
(some-> (:body-string req) empty->nil json/read-str))
(catch Exception e
(throw+ {:type :middleware/json-parse-error
:body (:body-string req)
:class :client-error
:msg (format "Failed to decode message body: %s"
(:body-string req))
:ex e}))))
encode-body (fn [resp]
(try+
(assoc resp :body
(some-> (:body resp) empty->nil json/write-str))
(catch Exception e
(throw+ {:type :middleware/json-write-error
:body (:body resp)
:class :server-error
:msg (format "Failed to encode message body: %s"
(:body resp))
:ex e}))))
add-header (fn [resp]
(assoc-in resp [:headers "Content-Type"]
"application/json"))]
(fn
([request]
(encode-body (handler (decode-body request))))
([request respond raise]
(handler (decode-body request)
#(respond (encode-body %))
raise)))))
(defn wrap-body-string [handler]
(letfn
[(body->string [req]
(if-let [body-string (request/body-string req)]
(assoc req :body-string body-string)
req))]
(fn
([request]
(handler (body->string request)))
([request respond raise]
(handler (body->string request)
respond
raise)))))
(defn verify-secret [fudo-secret handler]
(letfn [(check-secret-header [req]
(let [req-secret (get-in req [:headers "fudo-secret"])]
(cond (nil? req-secret)
(throw+ {:type :fudo/missing-secret
:class :client-error
:msg "Required fudo-secret header missing!"})
(not (= req-secret fudo-secret))
(throw+ {:type :fudo/invalid-secret
:class :client-error
:msg "Supplied fudo-secret invalid!"})
(= req-secret fudo-secret) req)))]
(fn
([request]
(handler (check-secret-header request)))
([request respond raise]
(handler (check-secret-header request) respond raise)))))
(defn- exception-to-string [ex]
(let [string-writer (java.io.StringWriter.)]
(.printStackTrace ex (java.io.PrintWriter. string-writer))
(.toString string-writer)))
(defn wrap-errors [debug handler]
(letfn [(client-error
[type msg]
(when debug
(util/err (format "Client error: bad request (%s): %s" type msg)))
(response/client-error
(format "Client error: bad request (%s): %s" type msg)))
(server-error
[type msg]
(util/err (format "Internal server error (%s): %s" type msg))
(response/internal-error
(format "Internal server error (%s): %s"
type msg)))
(unexpected-exception
[e]
(util/err (format "UNEXPECTED EXCEPTION (%s): %s\n%s"
(.. e getClass getName)
(.getMessage e)
(exception-to-string e)))
(response/internal-error
"Unexpected internal error!"))]
(fn
([request]
(try+ (handler request)
(catch [:class :client-error] {:keys [type msg]}
(client-error type msg))
(catch [:class :server-error] {:keys [type msg]}
(server-error type msg))
(catch Exception e
(unexpected-exception e))))
([request respond raise]
(try+ (handler request respond raise)
(catch [:class :client-error] {:keys [type msg]}
(server-error type msg))
(catch [:class :server-error] {:keys [type msg]}
(server-error type msg))
(catch Exception e
(unexpected-exception e)))))))
(defn sanitize-request [handler]
(letfn [(sanitize [req]
(apply dissoc req
(for [[k v] req :when (nil? v)] k)))]
(fn
([req] (handler (sanitize req)))
([req respond raise] (handler (sanitize req) respond raise)))))

View File

@ -0,0 +1,6 @@
(ns fudo.server.request-validator)
(defprotocol RequestValidator
(validate-request [validator entity req]
"Given a request, check to make sure it's valid."))

View File

@ -0,0 +1,28 @@
(ns fudo.server.service
(:require [clojure.spec.alpha :as s]))
(defprotocol FudoServiceComponent
"Represents a single API service component (eg. auth or dyndns)."
(public? [self]
"Indicate whether requests must be authenticated."))
(defprotocol FudoService
"Represents a Fudo service in a composable way--representing either
one component or all components."
(initialize [this]
"Perform any required initialization, and return an initialized
instance of the service.")
(enabled? [self]
"Is this service enabled on the current host?")
(generate-handler [self]
"Generate API routes to be added to the server."))
(defn fudo-service? [obj]
(and (satisfies? FudoService obj)
(satisfies? FudoServiceComponent obj)))
(s/def ::service fudo-service?)

View File

@ -0,0 +1,82 @@
(ns fudo.server.services
(:require [fudo.server.config :as config]
[fudo.api :as api]
[fudo.server.middleware :as middleware]
[fudo.server.service :as service]
[compojure.core :refer [routes context]]
[compojure.route :as route]
[com.stuartsierra.component :as component]
[clojure.spec.alpha :as s]))
(defn- pp [obj]
(println (format "DEBUG: %s" obj))
obj)
(defn- generate-api-handler [debug config services authenticator]
(let [fudo-secret (config/get-env config "FUDO_SECRET")
fudo-secret-middleware (partial middleware/verify-secret fudo-secret)
authenticator-middleware (partial middleware/validation-middleware
authenticator)
echo-middleware (partial middleware/echo-middleware debug)
error-wrapper (partial middleware/wrap-errors debug)
;; TODO: Changing the logic here to assume the services are
;; previously initialized (really, they should be initialized
;; on creation).
public-routes (apply routes (map service/generate-handler
(filter service/public? services)))
private-routes (apply routes (map service/generate-handler
(filter #(not (service/public? %))
services)))]
(routes
(context "/api" []
(context "/public" []
(-> public-routes
(middleware/json-wrapper)
(echo-middleware)
(middleware/wrap-body-string)
(fudo-secret-middleware)
(middleware/sanitize-request)
(error-wrapper)))
(context "/private" []
(-> private-routes
(middleware/json-wrapper)
(authenticator-middleware)
(echo-middleware)
(middleware/wrap-body-string)
(fudo-secret-middleware)
(middleware/sanitize-request)
(error-wrapper)))
(context "/" [] (route/not-found "Not found.")))
(context "/" [] (route/not-found "Not found.")))))
(s/fdef generate-service-handler
:args (s/cat :debug boolean?
:config ::config/config
:services (s/coll-of ::service/service)
:authenticator (s/fspec :args (s/cat :req ::api/request)
:ret ::api/authenticated-request))
:ret ::api/handler)
(defrecord FudoServices [debug config local-services authenticator]
component/Lifecycle
(start [this] this)
(stop [this] this)
service/FudoService
(enabled? [this]
(any? service/enabled? (:local-services this)))
(generate-handler [this]
(generate-api-handler (:debug this)
(:config this)
(-> this :local-services :services)
(:authenticator this))))
(defn init [debug]
(->FudoServices debug nil nil nil))
(defn init-mock [debug config local-services authenticator]
(->FudoServices debug config local-services authenticator))

View File

@ -0,0 +1,86 @@
(ns fudo.server.services.auth
(:require [fudo.server.service :as service]
[fudo.server.config :as config]
[fudo.api.response :as response]
[fudo.crypto.keys :as key]
[fudo.db.auth-db :as auth-db]
[fudo.db.pg-auth-db :as pg-auth-db]
[compojure.core :refer [routes context GET PUT]]
[compojure.route :as route]
[fudo.api :as api]
[clojure.spec.alpha :as s]))
(defn- get-entity-key-id [auth-db entity]
(-> (auth-db/get-entity-key-id auth-db entity)
(response/maybe-missing
(format "Entity not found: %s"
entity))))
(s/fdef get-entity-key-id
:args (s/cat :auth-db ::auth-db/auth-db :entity ::auth-db/entity-name)
:ret ::api/response)
(defn- get-entity-key [auth-db entity]
(-> (some-> (auth-db/get-entity-key auth-db entity)
key/pubkey->string)
(response/maybe-missing (format "Entity not found: %s"
entity))))
(s/fdef get-entity-key
:args (s/cat :auth-db ::auth-db/auth-db :entity ::auth-db/entity-name)
:ret ::api/response)
(defn- register-entity! [auth-db entity pubkey-str]
(-> (auth-db/register-entity! auth-db entity
(key/string->pubkey pubkey-str))
(::auth-db/key-id)
(response/ok)))
(s/fdef register-entity!
:args (s/cat :auth-db ::auth-db/auth-db
:entity ::auth-db/entity-name
:pubkey-str string?)
:ret ::api/response)
(defn- generate-auth-handler [debug auth-db]
(context "/auth" []
(context "/entity" []
(PUT "/:entity/key" [entity :as {body :body-string}]
(response/created
(register-entity! auth-db
entity
body)))
(GET "/:entity/key-id" [entity]
(get-entity-key-id auth-db entity))
(GET "/:entity/key" [entity]
(get-entity-key auth-db entity))
(GET "/ping" []
(response/ok "pong"))
(route/not-found "Entity object not found."))
(context "/" [] (route/not-found "Object not found."))))
(s/fdef generate-dyndns-handler
:args (s/cat :debug boolean? :auth-db ::auth-db/auth-db)
:ret ::api/handler)
(defrecord AuthService [debug config auth-db]
service/FudoService
(enabled? [this]
(-> config
(config/get-path (format "services::%s::auth"
(config/get-hostname)))
:auth
(comp not nil?)))
(generate-handler [this]
(generate-auth-handler (:debug this) (:auth-db this)))
(initialize [this]
(->AuthService (:debug this)
(:config this)
(auth-db/initialize (:auth-db this))))
service/FudoServiceComponent
(public? [this] true))
(defn create [debug config auth-db]
(->AuthService debug config auth-db))

View File

@ -0,0 +1,187 @@
(ns fudo.server.services.dyndns
(:require [fudo.server.service :as service]
[fudo.server.config :as config]
[fudo.net.ip :as ip]
[fudo.net.dns :as dns]
[fudo.net :as net]
[fudo.api :as api]
[fudo.api.request :as req]
[fudo.api.response :as response]
[fudo.db.auth-db :as auth-db]
[fudo.db.dyndns-db :as dyndns-db]
[fudo.net.ip :as ip]
[compojure.core :refer [routes context GET PUT]]
[compojure.route :as route]
[clojure.spec.alpha :as s]
[slingshot.slingshot :refer [throw+]]))
(defn- get-host-v4ip [dyndns-db host]
(-> (some-> (dyndns-db/get-host dyndns-db host)
::dns/v4ip
ip/to-string)
(response/maybe-missing (format "Host v4ip not found: %s" host))))
(s/fdef get-host-v4ip
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db
:host ::net/hostname)
:ret ::api/response)
(defn- get-host-v6ip [dyndns-db host]
(-> (some-> (dyndns-db/get-host dyndns-db host)
::dns/v6ip
ip/to-string)
(response/maybe-missing (format "Host v6ip not found: %s"
host))))
(s/fdef get-host-v6ip
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db :host ::net/hostname)
:ret ::api/response)
(defn- get-host-sshfp [dyndns-db host]
(response/maybe-missing (some->> (::dns/sshfp (dyndns-db/get-host dyndns-db host))
(clojure.string/join "\n"))
(format "Host sshfp records not found: %s" host)))
(s/fdef get-host-sshfp
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db :host ::net/hostname)
:ret ::api/response)
(defn- check-v4ip [v4ip]
(let [ip (ip/address v4ip)]
(when (not (ip/v4? ip))
(throw+ {:type :dyndns/invalid-address
:class :client-error
:msg (format "Not a v4 IP: %s" v4ip)}))
ip))
(defn- check-v6ip [v6ip]
(let [ip (ip/address v6ip)]
(when (not (ip/v6? ip))
(throw+ {:type :dyndns/invalid-address
:class :client-error
:msg (format "Not a v6 IP: %s" v6ip)}))
ip))
(defn- put-host-v4ip [dyndns-db host body]
(dyndns-db/register-v4ip! dyndns-db host (check-v4ip body))
(response/ok (format "Registered v4 address %s for host %s."
body host)))
(s/fdef put-host-v4ip
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db
:host ::net/hostname
:body ::ip/v4ip-str)
:ret ::api/response)
(defn- put-host-v6ip [dyndns-db host body]
(dyndns-db/register-v6ip! dyndns-db host (check-v6ip body))
(response/ok (format "Registered v6 address %s for host %s."
body host)))
(s/fdef put-host-v6ip
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db
:host ::net/hostname
:body ::ip/v6ip-str)
:ret ::api/response)
(defn- put-host-sshfp [dyndns-db host body]
(dyndns-db/register-sshfps! dyndns-db host body)
(response/ok (format "Registered sshfp for host %s."
host)))
(s/fdef put-host-sshfp
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db
:host ::net/hostname
:body (s/coll-of ::dns/sshfp-record))
:ret ::api/response)
(defn- entity [req]
(req/get-header req "fudo-entity"))
(s/fdef entity
:args (s/cat :req ::api/request)
:ret ::auth-db/entity-name)
(defn- authorized? [req host]
;; Since the entity is already validated vs. the key, it's safe to
;; just take the entity
(= host (entity req)))
(s/fdef authorized?
:args (s/cat :req ::api/request
:host ::net/hostname)
:ret boolean?)
(defn- get-domain [dyndns-db]
(response/ok (dyndns-db/get-domain dyndns-db)))
(s/fdef get-domain
:args (s/cat :dyndns-db ::dyndns-db/dyndns-db)
:ret ::api/response)
(defn- generate-dyndns-handler [debug dyndns-db]
(context "/dyndns" []
(GET "/domain" []
(get-domain dyndns-db))
(GET "/:host/v4ip" [host]
(get-host-v4ip dyndns-db host))
(GET "/:host/v6ip" [host]
(get-host-v6ip dyndns-db host))
(GET "/:host/sshfp" [host]
(get-host-sshfp dyndns-db host))
(PUT "/:host/v4ip" [host :as req]
(if (authorized? req host)
(put-host-v4ip dyndns-db
host
(:body-string req))
(response/unauth
(format "%s not authorized to modify host %s"
(entity req) host))))
(PUT "/:host/v6ip" [host :as req]
(if (authorized? req host)
(put-host-v6ip dyndns-db
host
(:body-string req))
(response/unauth
(format "%s not authorized to modify host %s"
(entity req) host))))
(PUT "/:host/sshfp" [host :as req]
(if (authorized? req host)
(put-host-sshfp dyndns-db
host
(clojure.string/split (:body-string req)
#"\n"))
(response/unauth
(format "%s not authorized to modify host %s"
(entity req) host))))
(GET "/ping" []
(response/ok "pong"))
(route/not-found "Host object not found")))
(s/fdef generate-dyndns-handler
:args (s/cat :debug boolean?
:dyndns-db ::dyndns-db/dyndns-db)
;;:ret ::api/handler
:ret any?)
(s/def ::debug boolean?)
(defrecord DynDnsService [debug config dyndns-db]
service/FudoService
(enabled? [this]
(-> config
(config/get-path (format "services::%s::dyndns"
(config/get-hostname)))
:dyndns
(comp not nil?)))
(generate-handler [this]
(generate-dyndns-handler (:debug this) (:dyndns-db this)))
(initialize [this]
(->DynDnsService (:debug this) (:config this)
(dyndns-db/initialize (:dyndns-db this))))
service/FudoServiceComponent
(public? [this] false))
(defn create [debug config dyndns-db]
(->DynDnsService debug config dyndns-db))
(s/fdef create
:args (s/cat :debug boolean?
:config ::config/config
:dyndns-db ::dyndns-db/dyndns-db)
:ret (s/keys :req-un [::debug ::config/config ::dyndns-db/dyndns-db]))

30
src/clj/fudo/utils.clj Normal file
View File

@ -0,0 +1,30 @@
(ns fudo.utils
(:require [brolog.logging :as log]
[clojure.spec.alpha :as s]))
(defn echo-through [data]
(println "\n\nDATA")
(clojure.pprint/pprint data)
(println "\n\n")
data)
(defn err [msg]
(binding [*out* *err*]
(println msg))
(log/error msg))
(defn info [msg]
(println msg)
(log/info msg))
(defn add-current-namespace [kwd]
(keyword (name (ns-name *ns*)) (name (name kwd))))
(s/fdef add-current-namespace
:args (s/cat :kwd simple-keyword?)
:ret qualified-keyword?)
(defn canonicalize-keys [pairs]
(into {} (map (fn [[k v]] {(add-current-namespace k) v}) pairs)))
(s/fdef canonicalize-keys
:args (s/cat :pairs (s/map-of simple-keyword? any?))
:ret (s/map-of qualified-keyword? any?))

315
test/fudo/server_test.clj Normal file
View File

@ -0,0 +1,315 @@
(ns fudo.server-test
(:require [fudo.api.request.generator :as gen]
[fudo.api.request :as req]
[fudo.server.config :as config]
[fudo.server.services.dyndns :as dyndns]
[fudo.server.services.auth :as auth]
[fudo.server.service :as service]
[fudo.server.services :as services]
[fudo.crypto.utils :as crypt]
[fudo.crypto.keys :as key]
[fudo.db.dyndns-db :as dyndns-db]
[fudo.db.transient-dyndns-db :as test-dyndns-db]
[fudo.db.auth-db :as auth-db]
[fudo.db.transient-auth-db :as test-auth-db]
[fudo.net.ip :as ip]
[fudo.net.dns :as dns]
[fudo.api.request.authenticator.auth-db :as authenticator]
[clojure.data.json :as json]
[clojure.test :refer [deftest is]]
[orchestra.spec.test :as spectest]))
(defn pp [obj]
(println (format "TEST_DBG: %s" obj))
obj)
(spectest/instrument)
(let [test-domain "test.com"
test-host "my-host"
config (config/init-mock-config {})
test-v4ip "4.3.2.1"
test-v6ip "6::2"
test-sshfp ["0 0 b9c019543f72777aee2d9b4ba883856f2f1f83e4e62ee8717"
"1 1 00a8f69cddae085451dec5d7b54e36e0bedb42241728751b81355f7abe8f99ffb"]
test-hosts {
"my-v4ip-host" {::dns/v4ip (ip/address test-v4ip)}
"my-v6ip-host" {::dns/v6ip (ip/address test-v6ip)}
"my-sshfp-host" {::dns/sshfp test-sshfp}
}
dyndns-db (test-dyndns-db/create true config test-domain test-hosts)
dyndns-service (service/initialize (dyndns/create true config dyndns-db))
dyndns-handler (service/generate-handler dyndns-service)]
(deftest dyndns-domain
(is (= test-domain
(-> (gen/make-get "/dyndns/domain")
(gen/add-host-header test-host)
dyndns-handler
:body
clojure.string/trim))))
(deftest dyndns-domain-success
(is (= 200
(-> (gen/make-get "/dyndns/domain")
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest insert-v4ip
(is (= 200
(-> (gen/make-put (format "/dyndns/%s/v4ip" test-host) "4.3.2.1")
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-v4ip
(is (= (ip/address test-v4ip)
(-> (gen/make-get (format "/dyndns/my-v4ip-host/v4ip"))
(gen/add-host-header test-host)
dyndns-handler
:body
clojure.string/trim
ip/address))))
(deftest read-v4ip-success
(is (= 200
(-> (gen/make-get (format "/dyndns/my-v4ip-host/v4ip"))
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest insert-v6ip
(is (= 200
(-> (gen/make-put (format "/dyndns/%s/v6ip" test-host) "4::1")
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-v6ip
(is (= (ip/address test-v6ip)
(-> (gen/make-get (format "/dyndns/my-v6ip-host/v6ip"))
(gen/add-host-header test-host)
dyndns-handler
:body
clojure.string/trim
ip/address))))
(deftest read-v6ip-success
(is (= 200
(-> (gen/make-get (format "/dyndns/my-v6ip-host/v6ip"))
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest insert-sshfp
(is (= 200
(-> (gen/make-put (format "/dyndns/%s/sshfp" test-host)
(clojure.string/join "\n" test-sshfp))
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-sshfp
(is (= test-sshfp
(-> (gen/make-get (format "/dyndns/my-sshfp-host/sshfp"))
(gen/add-host-header test-host)
dyndns-handler
:body
clojure.string/trim
(clojure.string/split #"\n")))))
(deftest read-sshfp-success
(is (= 200
(-> (gen/make-get (format "/dyndns/my-sshfp-host/sshfp"))
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-missing-v4ip
(is (= 404
(-> (gen/make-get "/dyndns/nonexistent-host/v4ip")
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-missing-v6ip
(is (= 404
(-> (gen/make-get "/dyndns/nonexistent-host/v6ip")
(gen/add-host-header test-host)
dyndns-handler
:status))))
(deftest read-missing-sshfp
(is (= 404
(-> (gen/make-get "/dyndns/nonexistent-host/sshfp")
(gen/add-host-header test-host)
dyndns-handler
:status)))))
(let [config (config/init-mock-config {})
keypair (key/generate-public-private-pair)
pubkey (-> keypair ::key/public-key)
key-id (-> pubkey key/pubkey->string crypt/sha1-hash)
test-host "test-host"
auth-hosts {test-host (auth-db/make-entity test-host pubkey)}
auth-db (test-auth-db/create true config auth-hosts)
auth-service (auth/create true config auth-db)
auth-handler (service/generate-handler auth-service)]
(deftest get-key
(is (= (key/pubkey->string pubkey)
(-> (gen/make-get (format "/auth/entity/%s/key" test-host))
(gen/add-host-header test-host)
auth-handler
:body
clojure.string/trim))))
(deftest get-key-success
(is (= 200
(-> (gen/make-get (format "/auth/entity/%s/key" test-host))
(gen/add-host-header test-host)
auth-handler
:status))))
(deftest get-missing-key
(is (= 404
(-> (gen/make-get "/auth/entity/nonexistent-host/key")
(gen/add-host-header test-host)
auth-handler
:status))))
(deftest get-key-id
(is (= key-id
(-> (gen/make-get (format "/auth/entity/%s/key-id" test-host))
(gen/add-host-header test-host)
auth-handler
:body
clojure.string/trim))))
(deftest get-key-id-success
(is (= 200
(-> (gen/make-get (format "/auth/entity/%s/key-id" test-host))
(gen/add-host-header test-host)
auth-handler
:status))))
(deftest get-missing-key-id
(is (= 404
(-> (gen/make-get "/auth/entity/nonexistent-host/key-id")
(gen/add-host-header test-host)
auth-handler
:status)))))
(let [debug false
test-domain "test.com"
test-host "my-host"
config (config/init-mock-config {"FUDO_SECRET" "test-secret"})
keypair (key/generate-public-private-pair)
privkey (-> keypair ::key/private-key)
pubkey (-> keypair ::key/public-key)
key-id (-> pubkey key/pubkey->string crypt/sha1-hash)
auth-hosts {test-host (auth-db/make-entity test-host pubkey)}
auth-db (test-auth-db/create debug config auth-hosts)
auth-service (auth/create debug config auth-db)
test-v4ip "4.3.2.1"
test-v6ip "6::2"
test-sshfp ["0 0 b9c019543f72777aee2d9b4ba883856f2f1f83e4e62ee8717"
"1 1 00a8f69cddae085451dec5d7b54e36e0bedb42241728751b81355f7abe8f99ffb"]
test-hosts {
"my-v4ip-host" {::dns/v4ip (ip/address test-v4ip)}
"my-v6ip-host" {::dns/v6ip (ip/address test-v6ip)}
"my-sshfp-host" {::dns/sshfp test-sshfp}
}
dyndns-db (test-dyndns-db/create debug config test-domain test-hosts)
dyndns-service (service/initialize (dyndns/create debug config dyndns-db))
authenticator (authenticator/init-mock debug config auth-db)
local-services {:services [auth-service dyndns-service]}
services (services/init-mock debug config local-services authenticator)
handler (service/generate-handler services)
get-result (fn [url]
(-> (gen/make-get url)
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
handler))
get-status (fn [url] (:status (get-result url)))
get-body (fn [url] (json/read-str (:body (get-result url))))
put-result (fn [url data]
(-> (gen/make-put url (json/write-str data))
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
handler))
put-status (fn [url data] (:status (pp (put-result url data))))]
(deftest authenticated-get-v4ip-success
(is (= 200 (get-status "/api/private/dyndns/my-v4ip-host/v4ip"))))
(deftest authenticated-get-v4ip
(is (= (ip/address test-v4ip)
(ip/address (get-body (format "/api/private/dyndns/my-v4ip-host/v4ip"))))))
(deftest authenticated-get-missing-v4ip
(is (= 404 (get-status "/api/private/dyndns/nonexistent-host/v4ip"))))
(deftest authenticated-put-new-v4ip-unauthorized
(is (= 401 (put-status "/api/private/dyndns/new-host/v4ip" "4.5.3.6"))))
(deftest authenticated-put-new-v4ip-authorized
(is (= 200 (put-status (format "/api/private/dyndns/%s/v4ip" test-host)
"4.5.3.6"))))
(deftest authenticated-put-new-v6ip-unauthorized
(is (= 401 (put-status "/api/private/dyndns/other-host/v6ip" "5::3"))))
(deftest authenticated-put-new-v6ip-authorized
(is (= 200 (put-status (format "/api/private/dyndns/%s/v6ip" test-host) "5::3"))))
(deftest authenticated-get-v6ip-success
(is (= 200 (get-status "/api/private/dyndns/my-v6ip-host/v6ip"))))
(deftest authenticated-get-v6ip
(is (= (ip/address test-v6ip)
(ip/address (get-body "/api/private/dyndns/my-v6ip-host/v6ip")))))
(deftest authenticated-get-missing-v6ip
(is (= 404 (get-status "/api/private/dyndns/nonexistent-host/v6ip"))))
(deftest missing-fudo-secret
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/authenticate-request test-host privkey key-id)
handler
:status))))
(deftest missing-fudo-entity-header
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
(gen/remove-header "fudo-entity")
handler
:status))))
(deftest missing-fudo-key-id-header
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
(gen/remove-header "fudo-key-id")
handler
:status))))
(deftest missing-fudo-timestamp-header
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
(gen/remove-header "fudo-timestamp")
handler
:status))))
(deftest missing-fudo-signature-header
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
(gen/remove-header "fudo-signature")
handler
:status))))
(deftest bad-fudo-signature-header
(is (= 400
(-> (gen/make-get "/api/private/dyndns/my-v4ip-host/v4ip")
(gen/add-fudo-secret "test-secret")
(gen/authenticate-request test-host privkey key-id)
(req/set-header "fudo-signature" "gobbledigook")
handler
:status)))))