Skip to content

Commit

Permalink
Merge branch 'readium:master' into ci
Browse files Browse the repository at this point in the history
  • Loading branch information
ddfreiling authored May 14, 2024
2 parents 731b021 + 799e77a commit 5eb1124
Show file tree
Hide file tree
Showing 26 changed files with 484 additions and 237 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Prerequisites
Binaries are only pre-built on demand and for a service fee, therefore in the general case you'll need to get a working Golang installation.
Please refer to the official Go documentation for installation procedures at https://golang.org/.

This software is working with *go 1.16* or higher. It is currently maintained using *go 1.20* (July 2023).
This software is working with *go 1.19* or higher. It is currently maintained using *go 1.21* (February 2024).

You must put in place:

Expand All @@ -30,15 +30,12 @@ You must put in place:

3/ a License Gateway, i.e. a piece of sofware you'll have to develop, which takes a request for an existing LCP license from a reading app, interrogates your database in order to get user information relative to this license, calls the License Server endpoint and returns this fresh LCP license back to the caller app (more information in the project Wiki).

4/ a large storage volume for encrypted publications. It can be either a file system accessible from the Web via HTTP URLs, or an S3 bucket. Note that publications are encrypted once: every license generated for such publication is pointing at the same encrypted file. Because these publications are stronlgy encrypted and the decryption key is secured in your SQL database, public access to these files is not problematic.
4/ a large storage volume for encrypted publications. It can be either a file system accessible from the Web via HTTP URLs, or an S3 bucket. Note that publications are encrypted once: every license generated for such publication is pointing at the same encrypted file. Because these publications are stronlgy encrypted and the decryption key is secured in your SQL database, public access to these files is not problematic.

The servers require the setup of an SQL Database.

- SQLite is sufficient for most needs. If the "database" property of each server defines a sqlite3 driver, the db setup is dynamically achieved when the server runs for the first time. SQLite database creation scripts are also provided in the "dbmodel" folder in case they are useful.
- MySQL database creation scripts are provided in the "dbmodel" folder. These scripts must be applied before launching the servers for the first time.
- MS SQL Server database creation scripts are provided as well in the "dbmodel" folder. These scripts must be applied before launching the servers for the first time.

A PostgresQL integration has been provided by a user of the LCP Server as a branch of the codebase, but is not fully integrated in the up-to-date codebase. Contact EDRLab if you want to sponsor its full integration.
- MySQL, MS SQL and PostgreSQL database creation scripts are provided in the "dbmodel" folder. These scripts must be applied before launching the servers for the first time.

Encryption Profiles
===================
Expand Down Expand Up @@ -237,13 +234,16 @@ Here are the details about the configuration properties of each server. In the s

Here are models for the database property (variables in curly brackets):
- sqlite: `sqlite3://file:{path-to-dot-sqlite-file}?cache=shared&mode=rwc`
- MySQL: `mysql://{login}:{password}@/{dbname}?parseTime=true`
- MS SQL Server: `mssql://server={server-address};user id={login};password={password};database={dbname}`
- MySQL: `mysql://{username}:{password}@/{dbname}?parseTime=true`
- MS SQL Server: `mssql://server={server-address};user id={username};password={password};database={dbname}`
- PostgrSQL: `postgres://{username}:{password}@{host}:{port}/{dbname}`

Note 1 relative to MS SQL Server: when using SQL Server Express, the server-address is of type `{ip-address}\\SQLEXPRESS)`.

Note 2 relative to MS SQL Server: we've seen installs with the additional parameters `;connection timeout=30;encrypt=disable`.

Note 3 relative to PostgrSQL: add `?sslmode=disable` for a local test install.

#### storage section
This section should be empty if the storage location of encrypted publications is managed by the lcpencrypt utility.
If this section is present and lcpencrypt does not manage the storage, all encrypted publications will be stored in the configured folder or s3 bucket.
Expand Down Expand Up @@ -338,7 +338,7 @@ lsd_notify_auth:

#### license_status section
`license_status`: parameters related to the interactions implemented by the Status server, if any:
- `renting_days`: maximum number of days allowed for a loan, from the date the loan starts. If set to 0 or absent, no loan renewal is possible.
- `renting_days`: maximum number of days allowed for a loan, used for laon extensions. The maximum end date is calculated from the date the loan starts plus this value. If set to 0 or absent, no loan renewal is possible.
- `renew`: boolean; if `true`, the renewal of a loan is possible.
- `renew_days`: default number of additional days allowed during a renewal.
- `return`: boolean; if `true`, an early return is possible.
Expand Down
2 changes: 1 addition & 1 deletion api/common_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

const (
// DO NOT FORGET to update the version
Software_Version = "1.9.0"
Software_Version = "1.9.2"

ContentType_LCP_JSON = "application/vnd.readium.lcp.license.v1.0+json"
ContentType_LSD_JSON = "application/vnd.readium.license.status.v1.0+json"
Expand Down
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type Configuration struct {
LicenseStatus LicenseStatus `yaml:"license_status"`
Localization Localization `yaml:"localization"`
Logging Logging `yaml:"logging"`
TestMode bool `yaml:"test_mode"`
GoofyMode bool `yaml:"goofy_mode"`
Profile string `yaml:"profile,omitempty"`

Expand Down Expand Up @@ -141,6 +142,10 @@ func GetDatabase(uri string) (string, string) {
}

parts := strings.Split(uri, "://")
if parts[0] == "postgres" {
return parts[0], uri
}

return parts[0], parts[1]
}

Expand Down
50 changes: 50 additions & 0 deletions dbmodel/postgres _db_setup_frontend.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
CREATE SEQUENCE publication_seq;

CREATE TABLE publication (
id int PRIMARY KEY DEFAULT NEXTVAL ('publication_seq'),
uuid varchar(255) NOT NULL, /* == content id */
title varchar(255) NOT NULL,
status varchar(255) NOT NULL
);

CREATE INDEX uuid_index ON publication (uuid);

CREATE SEQUENCE user_seq;

CREATE TABLE "user" (
id int PRIMARY KEY DEFAULT NEXTVAL ('user_seq'),
uuid varchar(255) NOT NULL,
name varchar(64) NOT NULL,
email varchar(64) NOT NULL,
password varchar(64) NOT NULL,
hint varchar(64) NOT NULL
);

CREATE SEQUENCE purchase_seq;

CREATE TABLE purchase (
id int PRIMARY KEY DEFAULT NEXTVAL ('purchase_seq'),
uuid varchar(255) NOT NULL,
publication_id int NOT NULL,
user_id int NOT NULL,
license_uuid varchar(255) NULL,
type varchar(32) NOT NULL,
transaction_date timestamp(0),
start_date timestamp(0),
end_date timestamp(0),
status varchar(255) NOT NULL,
FOREIGN KEY (publication_id) REFERENCES publication (id),
FOREIGN KEY (user_id) REFERENCES "user" (id)
);

CREATE INDEX idx_purchase ON purchase (license_uuid);

CREATE SEQUENCE license_view_seq;

CREATE TABLE license_view (
id int PRIMARY KEY DEFAULT NEXTVAL ('license_view_seq'),
uuid varchar(255) NOT NULL,
device_count int NOT NULL,
status varchar(255) NOT NULL,
message varchar(255) NOT NULL
);
25 changes: 25 additions & 0 deletions dbmodel/postgres_db_setup_lcpserver.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

CREATE TABLE content (
id varchar(255) PRIMARY KEY NOT NULL,
encryption_key bytea NOT NULL,
location text NOT NULL,
length bigint,
sha256 varchar(64),
type varchar(255) NOT NULL DEFAULT 'application/epub+zip'
);

-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE TABLE license (
id varchar(255) PRIMARY KEY NOT NULL,
user_id varchar(255) NOT NULL,
provider varchar(255) NOT NULL,
issued timestamp(0) NOT NULL,
updated timestamp(0) DEFAULT NULL,
rights_print int DEFAULT NULL,
rights_copy int DEFAULT NULL,
rights_start timestamp(0) DEFAULT NULL,
rights_end timestamp(0) DEFAULT NULL,
content_fk varchar(255) NOT NULL,
lsd_status int default 0,
FOREIGN KEY(content_fk) REFERENCES content(id)
);
26 changes: 26 additions & 0 deletions dbmodel/postgres_db_setup_lsdserver.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE TABLE license_status (
id serial4 NOT NULL,
status smallint NOT NULL,
license_updated timestamp(3) NOT NULL,
status_updated timestamp(3) NOT NULL,
device_count smallint DEFAULT NULL,
potential_rights_end timestamp(3) DEFAULT NULL,
license_ref varchar(255) NOT NULL,
rights_end timestamp(3) DEFAULT NULL,
CONSTRAINT license_status_pkey PRIMARY KEY (id)
);

CREATE INDEX license_ref_index ON license_status (license_ref);

CREATE TABLE event (
id serial4 NOT NULL,
device_name varchar(255) DEFAULT NULL,
timestamp timestamp(3) NOT NULL,
type int NOT NULL,
device_id varchar(255) DEFAULT NULL,
license_status_fk int NOT NULL,
CONSTRAINT event_pkey PRIMARY KEY (id),
FOREIGN KEY(license_status_fk) REFERENCES license_status(id)
);

CREATE INDEX license_status_fk_index on event (license_status_fk);
31 changes: 31 additions & 0 deletions dbutils/dbutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dbutils

import (
"bytes"
"fmt"
"strings"
)

func getPostgresQuery(query string) string {
var buffer bytes.Buffer
idx := 1
for _, char := range query {
if char == '?' {
buffer.WriteString(fmt.Sprintf("$%d", idx))
idx += 1
} else {
buffer.WriteRune(char)
}
}
return buffer.String()
}

// GetParamQuery replaces parameter placeholders '?' in the SQL query to
// placeholders supported by the selected database driver.
func GetParamQuery(database, query string) string {
if strings.HasPrefix(database, "postgres") {
return getPostgresQuery(query)
} else {
return query
}
}
17 changes: 17 additions & 0 deletions dbutils/dbutils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dbutils

import "testing"

const demo_query = "SELECT * FROM test WHERE id = ? AND test = ? LIMIT 1"

func TestGetParamQuery(t *testing.T) {
q := GetParamQuery("postgres", demo_query)
if q != "SELECT * FROM test WHERE id = $1 AND test = $2 LIMIT 1" {
t.Fatalf("Incorrect postgres query")
}

q = GetParamQuery("sqlite3", demo_query)
if q != "SELECT * FROM test WHERE id = ? AND test = ? LIMIT 1" {
t.Fatalf("Incorrect sqlite3 query")
}
}
2 changes: 1 addition & 1 deletion encrypt/notify_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func AbortNotification(pub Publication, lcpsv string, v2 bool, username string,
var notifyURL string
var err error
if v2 {
notifyURL, err = url.JoinPath(lcpsv, "publications")
notifyURL, err = url.JoinPath(lcpsv, "publications", pub.UUID)
} else {
notifyURL, err = url.JoinPath(lcpsv, "contents", pub.UUID)
}
Expand Down
19 changes: 11 additions & 8 deletions frontend/weblicense/weblicense.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"log"

"github.com/readium/readium-lcp-server/config"
"github.com/readium/readium-lcp-server/dbutils"
)

// License status
Expand Down Expand Up @@ -135,7 +136,7 @@ func (licManager LicenseManager) GetFiltered(deviceLimit string) ([]License, err
// Add adds a new license
func (licManager LicenseManager) Add(licenses License) error {

_, err := licManager.db.Exec("INSERT INTO license_view (uuid, device_count, status, message) VALUES (?, ?, ?, ?)",
_, err := licManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "INSERT INTO license_view (uuid, device_count, status, message) VALUES (?, ?, ?, ?)"),
licenses.UUID, licenses.DeviceCount, licenses.Status, licenses.Message)
return err
}
Expand All @@ -148,7 +149,8 @@ func (licManager LicenseManager) AddFromJSON(licensesJSON []byte) error {
return err
}

add, err := licManager.db.Prepare("INSERT INTO license_view (uuid, device_count, status, message) VALUES (?, ?, ?, ?)")
add, err := licManager.db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database,
"INSERT INTO license_view (uuid, device_count, status, message) VALUES (?, ?, ?, ?)"))
if err != nil {
return err
}
Expand All @@ -173,15 +175,16 @@ func (licManager LicenseManager) PurgeDataBase() error {
// Update updates a license
func (licManager LicenseManager) Update(lic License) error {

_, err := licManager.db.Exec("UPDATE license_view SET device_count=?, uuid=?, status=? , message=? WHERE id = ?",
_, err := licManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database,
"UPDATE license_view SET device_count=?, uuid=?, status=? , message=? WHERE id = ?"),
lic.DeviceCount, lic.Status, lic.UUID, lic.ID, lic.Message)
return err
}

// Delete deletes a license
func (licManager LicenseManager) Delete(id int64) error {

_, err := licManager.db.Exec("DELETE FROM license_view WHERE id = ?", id)
_, err := licManager.db.Exec(dbutils.GetParamQuery(config.Config.FrontendServer.Database, "DELETE FROM license_view WHERE id = ?"), id)
return err
}

Expand All @@ -200,25 +203,25 @@ func Init(db *sql.DB) (i WebLicense, err error) {
}

var dbGetByID *sql.Stmt
dbGetByID, err = db.Prepare(
dbGetByID, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database,
`SELECT l.uuid, pu.title, u.name, p.type, l.device_count, l.status, p.id, l.message
FROM license_view AS l
INNER JOIN purchase as p ON l.uuid = p.license_uuid
INNER JOIN publication as pu ON p.publication_id = pu.id
INNER JOIN "user" as u ON p.user_id = u.id
WHERE l.id = ?`)
WHERE l.id = ?`))
if err != nil {
log.Println("Error preparing dbGetByID")
return
}

var dbGetFiltered *sql.Stmt
dbGetFiltered, err = db.Prepare(
dbGetFiltered, err = db.Prepare(dbutils.GetParamQuery(config.Config.FrontendServer.Database,
`SELECT l.uuid, pu.title, u.name, p.type, l.device_count, l.status, p.id, l.message FROM license_view AS l
INNER JOIN purchase as p ON l.uuid = p.license_uuid
INNER JOIN publication as pu ON p.publication_id = pu.id
INNER JOIN "user" as u ON p.user_id = u.id
WHERE l.device_count >= ?`)
WHERE l.device_count >= ?`))
if err != nil {
log.Println("Error preparing dbGetFiltered")
return
Expand Down
Loading

0 comments on commit 5eb1124

Please sign in to comment.