initial commit, moved from system repo
This commit is contained in:
commit
f923af7325
|
@ -0,0 +1,11 @@
|
|||
/target
|
||||
/classes
|
||||
/checkouts
|
||||
pom.xml
|
||||
pom.xml.asc
|
||||
*.jar
|
||||
*.class
|
||||
/.lein-*
|
||||
/.nrepl-port
|
||||
.hgignore
|
||||
.hg/
|
|
@ -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
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
# Introduction to dyndns-server
|
||||
|
||||
TODO: write [great documentation](http://jacobian.org/writing/what-to-write/)
|
|
@ -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"}]])
|
|
@ -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
|
||||
}
|
|
@ -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))
|
|
@ -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)
|
|
@ -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))
|
|
@ -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)
|
|
@ -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."))
|
|
@ -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)})
|
|
@ -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?)
|
|
@ -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?))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)))
|
|
@ -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)
|
|
@ -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?)
|
|
@ -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]))
|
|
@ -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?)
|
|
@ -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))
|
||||
|
|
@ -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) #"\.")))
|
|
@ -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))
|
|
@ -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)))))
|
|
@ -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."))
|
|
@ -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?)
|
|
@ -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))
|
|
@ -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))
|
|
@ -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]))
|
|
@ -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?))
|
|
@ -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)))))
|
Loading…
Reference in New Issue