continue integration new model (better security)

This commit is contained in:
Edouard DUPIN 2022-12-13 22:38:37 +01:00
parent c6472630ca
commit 7f7b73ec51
12 changed files with 20 additions and 547 deletions

View File

@ -26,7 +26,7 @@
<dependency>
<groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId>
<version>0.1.2</version>
<version>0.1.3</version>
</dependency>
</dependencies>

View File

@ -6,7 +6,6 @@ import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.kar.karideo.api.*;
import org.kar.karideo.model.Data;
import org.kar.karideo.model.Media;
import org.kar.karideo.model.Season;
import org.kar.karideo.model.Series;
@ -14,13 +13,14 @@ import org.kar.karideo.model.Type;
import org.kar.archidata.GlobalConfiguration;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.UpdateJwtPublicKey;
import org.kar.archidata.api.DataResource;
import org.kar.archidata.db.DBConfig;
import org.kar.archidata.filter.AuthenticationFilter;
import org.kar.archidata.filter.CORSFilter;
import org.kar.archidata.filter.OptionFilter;
import org.kar.archidata.model.Data;
import org.kar.archidata.model.User;
import org.kar.archidata.util.ConfigBaseVariable;
import org.kar.archidata.util.JWTWrapper;
import org.glassfish.jersey.jackson.JacksonFeature;
import javax.ws.rs.core.UriBuilder;

View File

@ -1,15 +0,0 @@
package org.kar.karideo.annotation;
import javax.ws.rs.NameBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@NameBinding
@Retention(RUNTIME)
@Target({METHOD})
public @interface PermitTokenInURI {
}

View File

@ -1,426 +0,0 @@
package org.kar.karideo.api;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.karideo.annotation.PermitTokenInURI;
import org.kar.archidata.filter.GenericContext;
import org.kar.karideo.model.Data;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.util.ConfigBaseVariable;
import javax.imageio.ImageIO;
import javax.ws.rs.*;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.StreamingOutput;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
// https://stackoverflow.com/questions/35367113/jersey-webservice-scalable-approach-to-download-file-and-reply-to-client
// https://gist.github.com/aitoroses/4f7a2b197b732a6a691d
@Path("/data")
@Produces({MediaType.APPLICATION_JSON})
public class DataResource {
private final static int CHUNK_SIZE = 1024 * 1024; // 1MB chunks
private final static int CHUNK_SIZE_IN = 50 * 1024 * 1024; // 1MB chunks
/**
* Upload some datas
*/
private static long tmpFolderId = 1;
private static void createFolder(String path) throws IOException {
if (!Files.exists(java.nio.file.Path.of(path))) {
//Log.print("Create folder: " + path);
Files.createDirectories(java.nio.file.Path.of(path));
}
}
public static long getTmpDataId() {
return tmpFolderId++;
}
public static String getTmpFileInData(long tmpFolderId) {
String filePath = ConfigBaseVariable.getTmpDataFolder() + File.separator + tmpFolderId;
try {
createFolder(ConfigBaseVariable.getTmpDataFolder() + File.separator);
} catch (IOException e) {
e.printStackTrace();
}
return filePath;
}
public static String getFileData(long tmpFolderId) {
String filePath = ConfigBaseVariable.getMediaDataFolder() + File.separator + tmpFolderId + File.separator + "data";
try {
createFolder(ConfigBaseVariable.getMediaDataFolder() + File.separator + tmpFolderId + File.separator);
} catch (IOException e) {
e.printStackTrace();
}
return filePath;
}
public static Data getWithSha512(String sha512) {
System.out.println("find sha512 = " + sha512);
try {
return SqlWrapper.getWhere(Data.class, "sha512", "=", sha512);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static Data getWithId(long id) {
System.out.println("find id = " + id);
try {
return SqlWrapper.get(Data.class, id);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static Data createNewData(long tmpUID, String originalFileName, String sha512) throws IOException {
// determine mime type:
Data injectedData = new Data();
String mimeType = "";
String extension = originalFileName.substring(originalFileName.lastIndexOf('.') + 1);
switch (extension.toLowerCase()) {
case "jpg":
case "jpeg":
mimeType = "image/jpeg";
break;
case "png":
mimeType = "image/png";
break;
case "webp":
mimeType = "image/webp";
break;
case "mka":
mimeType = "audio/x-matroska";
break;
case "mkv":
mimeType = "video/x-matroska";
break;
case "webm":
mimeType = "video/webm";
break;
default:
throw new IOException("Can not find the mime type of data input: '" + extension + "'");
}
injectedData.mimeType = mimeType;
injectedData.sha512 = sha512;
String tmpPath = getTmpFileInData(tmpUID);
injectedData.size = Files.size(Paths.get(tmpPath));
try {
injectedData = SqlWrapper.insert(injectedData);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
String mediaPath = getFileData(injectedData.id);
System.out.println("src = " + tmpPath);
System.out.println("dst = " + mediaPath);
Files.move(Paths.get(tmpPath), Paths.get(mediaPath), StandardCopyOption.ATOMIC_MOVE);
System.out.println("Move done");
return injectedData;
}
static String saveTemporaryFile(InputStream uploadedInputStream, long idData) {
return saveFile(uploadedInputStream, DataResource.getTmpFileInData(idData));
}
static void removeTemporaryFile(long idData) {
String filepath = DataResource.getTmpFileInData(idData);
if (Files.exists(Paths.get(filepath))) {
try {
Files.delete(Paths.get(filepath));
} catch (IOException e) {
System.out.println("can not delete temporary file : " + Paths.get(filepath));
e.printStackTrace();
}
}
}
// save uploaded file to a defined location on the server
static String saveFile(InputStream uploadedInputStream, String serverLocation) {
String out = "";
try {
OutputStream outpuStream = new FileOutputStream(new File(
serverLocation));
int read = 0;
byte[] bytes = new byte[CHUNK_SIZE_IN];
MessageDigest md = MessageDigest.getInstance("SHA-512");
outpuStream = new FileOutputStream(new File(serverLocation));
while ((read = uploadedInputStream.read(bytes)) != -1) {
//System.out.println("write " + read);
md.update(bytes, 0, read);
outpuStream.write(bytes, 0, read);
}
System.out.println("Flush input stream ... " + serverLocation);
System.out.flush();
outpuStream.flush();
outpuStream.close();
// create the end of sha512
byte[] sha512Digest = md.digest();
// convert in hexadecimal
out = bytesToHex(sha512Digest);
uploadedInputStream.close();
} catch (IOException ex) {
System.out.println("Can not write in temporary file ... ");
ex.printStackTrace();
} catch (NoSuchAlgorithmException ex) {
System.out.println("Can not find sha512 algorithms");
ex.printStackTrace();
}
return out;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public Data getSmall(Long id) {
try {
return SqlWrapper.get(Data.class, id);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
@POST
@Path("/upload/")
@Consumes({MediaType.MULTIPART_FORM_DATA})
@RolesAllowed("ADMIN")
public Response uploadFile(@Context SecurityContext sc, @FormDataParam("file") InputStream fileInputStream, @FormDataParam("file") FormDataContentDisposition fileMetaData) {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
System.out.println("===================================================");
System.out.println("== DATA uploadFile " + (gc==null?"null":gc.user));
System.out.println("===================================================");
//public NodeSmall uploadFile(final FormDataMultiPart form) {
System.out.println("Upload file: ");
String filePath = ConfigBaseVariable.getTmpDataFolder() + File.separator + tmpFolderId++;
try {
createFolder(ConfigBaseVariable.getTmpDataFolder() + File.separator);
} catch (IOException e) {
e.printStackTrace();
}
saveFile(fileInputStream, filePath);
return Response.ok("Data uploaded successfully !!").build();
//return null;
}
@GET
@Path("{id}")
@PermitTokenInURI
@RolesAllowed("USER")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response retriveDataId(@Context SecurityContext sc, @QueryParam(HttpHeaders.AUTHORIZATION) String token, @HeaderParam("Range") String range, @PathParam("id") Long id) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
//System.out.println("===================================================");
System.out.println("== DATA retriveDataId ? id=" + id + " user=" + (gc==null?"null":gc.user));
//System.out.println("===================================================");
Data value = getSmall(id);
if (value == null) {
Response.status(404).
entity("media NOT FOUND: " + id).
type("text/plain").
build();
}
return buildStream(ConfigBaseVariable.getMediaDataFolder() + File.separator + id + File.separator + "data", range, value.mimeType);
}
@GET
@Path("thumbnail/{id}")
@RolesAllowed("USER")
@PermitTokenInURI
@Produces(MediaType.APPLICATION_OCTET_STREAM)
//@CacheMaxAge(time = 10, unit = TimeUnit.DAYS)
public Response retriveDataThumbnailId(@Context SecurityContext sc, @QueryParam(HttpHeaders.AUTHORIZATION) String token, @HeaderParam("Range") String range, @PathParam("id") Long id) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
//System.out.println("===================================================");
//System.out.println("== DATA retriveDataThumbnailId ? " + (gc==null?"null":gc.user));
//System.out.println("===================================================");
Data value = getSmall(id);
if (value == null) {
return Response.status(404).
entity("media NOT FOUND: " + id).
type("text/plain").
build();
}
String filePathName = ConfigBaseVariable.getMediaDataFolder() + File.separator + id + File.separator + "data";
if ( value.mimeType.contentEquals("image/jpeg")
|| value.mimeType.contentEquals("image/png")
// || value.mimeType.contentEquals("image/webp")
) {
// reads input image
//System.out.println("Read path: " + filePathName);
File inputFile = new File(filePathName);
if (!inputFile.exists()) {
return Response.status(500).
entity("Internal Error: Media is NOT FOUNDABLE: " + id).
type("text/plain").
build();
}
BufferedImage inputImage = ImageIO.read(inputFile);
int scaledWidth = 250;
int scaledHeight = (int)((float)inputImage.getHeight() / (float)inputImage.getWidth() * (float) scaledWidth);
// creates output image
BufferedImage outputImage = new BufferedImage(scaledWidth,
scaledHeight, inputImage.getType());
// scales the input image to the output image
Graphics2D g2d = outputImage.createGraphics();
g2d.drawImage(inputImage, 0, 0, scaledWidth, scaledHeight, null);
g2d.dispose();
// create the output stream:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
// TODO: check how to remove buffer file !!! here, it is not needed at all...
ImageIO.write( outputImage, "JPG", baos);
} catch (IOException e) {
e.printStackTrace();
return Response.status(500).
entity("Internal Error: resize fail: " + e.getMessage()).
type("text/plain").
build();
}
byte[] imageData = baos.toByteArray();
//Response.ok(new ByteArrayInputStream(imageData)).build();
Response.ResponseBuilder out = Response.ok(imageData)
.header(HttpHeaders.CONTENT_LENGTH, imageData.length);
out.type("image/jpeg");
// TODO: move this in a decorator !!!
CacheControl cc = new CacheControl();
cc.setMaxAge(3600);
cc.setNoCache(false);
out.cacheControl(cc);
return out.build();
}
return buildStream(filePathName, range, value.mimeType);
}
//@Secured
@GET
@Path("{id}/{name}")
@PermitTokenInURI
@RolesAllowed("USER")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response retriveDataFull(@Context SecurityContext sc, @QueryParam(HttpHeaders.AUTHORIZATION) String token, @HeaderParam("Range") String range, @PathParam("id") Long id, @PathParam("name") String name) throws Exception {
GenericContext gc = (GenericContext) sc.getUserPrincipal();
//System.out.println("===================================================");
System.out.println("== DATA retriveDataFull ? id=" + id + " user=" + (gc==null?"null":gc.user));
//System.out.println("===================================================");
Data value = getSmall(id);
if (value == null) {
Response.status(404).
entity("media NOT FOUND: " + id).
type("text/plain").
build();
}
return buildStream(ConfigBaseVariable.getMediaDataFolder() + File.separator + id + File.separator + "data", range, value.mimeType);
}
/**
* Adapted from http://stackoverflow.com/questions/12768812/video-streaming-to-ipad-does-not-work-with-tapestry5/12829541#12829541
*
* @param range range header
* @return Streaming output
* @throws Exception IOException if an error occurs in streaming.
*/
private Response buildStream(final String filename, final String range, String mimeType) throws Exception {
File file = new File(filename);
//System.out.println("request range : " + range);
// range not requested : Firefox does not send range headers
if (range == null) {
final StreamingOutput output = new StreamingOutput() {
@Override
public void write(OutputStream out) {
try (FileInputStream in = new FileInputStream(file)) {
byte[] buf = new byte[1024 * 1024];
int len;
while ((len = in.read(buf)) != -1) {
try {
out.write(buf, 0, len);
out.flush();
//System.out.println("---- wrote " + len + " bytes file ----");
} catch (IOException ex) {
System.out.println("remote close connection");
break;
}
}
} catch (IOException ex) {
throw new InternalServerErrorException(ex);
}
}
};
Response.ResponseBuilder out = Response.ok(output)
.header(HttpHeaders.CONTENT_LENGTH, file.length());
if (mimeType != null) {
out.type(mimeType);
}
return out.build();
}
String[] ranges = range.split("=")[1].split("-");
final long from = Long.parseLong(ranges[0]);
//System.out.println("request range : " + ranges.length);
//Chunk media if the range upper bound is unspecified. Chrome, Opera sends "bytes=0-"
long to = CHUNK_SIZE + from;
if (ranges.length == 1) {
to = file.length() - 1;
} else {
if (to >= file.length()) {
to = (long) (file.length() - 1);
}
}
final String responseRange = String.format("bytes %d-%d/%d", from, to, file.length());
//System.out.println("responseRange : " + responseRange);
final RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(from);
final long len = to - from + 1;
final MediaStreamer streamer = new MediaStreamer(len, raf);
Response.ResponseBuilder out = Response.ok(streamer)
.status(Response.Status.PARTIAL_CONTENT)
.header("Accept-Ranges", "bytes")
.header("Content-Range", responseRange)
.header(HttpHeaders.CONTENT_LENGTH, streamer.getLenth())
.header(HttpHeaders.LAST_MODIFIED, new Date(file.lastModified()));
if (mimeType != null) {
out.type(mimeType);
}
return out.build();
}
public static void undelete(Long id) throws Exception {
SqlWrapper.unsetDelete(Data.class, id);
}
}

View File

@ -1,56 +0,0 @@
package org.kar.karideo.api;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.StreamingOutput;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
public class MediaStreamer implements StreamingOutput {
private final int CHUNK_SIZE = 1024 * 1024; // 1MB chunks
final byte[] buf = new byte[CHUNK_SIZE];
private long length;
private RandomAccessFile raf;
public MediaStreamer(long length, RandomAccessFile raf) throws IOException {
//System.out.println("request stream of " + length / 1024 + " data");
if (length<0) {
throw new IOException("Wrong size of the file to stream: " + length);
}
this.length = length;
this.raf = raf;
}
@Override
public void write(OutputStream outputStream) {
try {
while (length != 0) {
int read = raf.read(buf, 0, buf.length > length ? (int) length : buf.length);
try {
outputStream.write(buf, 0, read);
} catch (IOException ex) {
System.out.println("remote close connection");
break;
}
length -= read;
}
} catch (IOException ex) {
throw new InternalServerErrorException(ex);
} catch (WebApplicationException ex) {
throw new InternalServerErrorException(ex);
} finally {
try {
raf.close();
} catch (IOException ex) {
ex.printStackTrace();
throw new InternalServerErrorException(ex);
}
}
}
public long getLenth() {
return length;
}
}

View File

@ -94,8 +94,8 @@ public class SeasonResource {
out.name = name;
out.parentId = seriesId;
out = SqlWrapper.insert(out);
return out;
}
return out;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();

View File

@ -95,8 +95,8 @@ public class SeriesResource {
out.name = name;
out.parentId = typeId;
out = SqlWrapper.insert(out);
return out;
}
return out;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();

View File

@ -98,8 +98,8 @@ public class TypeResource {
out = new Type();
out.name = name;
out = SqlWrapper.insert(out);
return out;
}
return out;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();

View File

@ -2,16 +2,16 @@ package org.kar.karideo.api;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.kar.karideo.WebLauncher;
import org.kar.archidata.db.DBEntry;
import org.kar.archidata.model.Data;
import org.kar.archidata.util.DataTools;
import org.kar.karideo.model.Data;
import org.kar.karideo.model.Media;
import org.kar.karideo.model.Season;
import org.kar.karideo.model.Series;
import org.kar.karideo.model.Type;
import org.kar.archidata.SqlWrapper;
import org.kar.archidata.annotation.security.RolesAllowed;
import org.kar.archidata.api.DataResource;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -43,6 +43,7 @@ public class VideoResource {
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Media put(@PathParam("id") Long id, String jsonRequest) throws Exception {
System.out.println("update video " + id + " ==> '" + jsonRequest + "'");
SqlWrapper.update(Media.class, id, jsonRequest);
return SqlWrapper.get(Media.class, id);
}

View File

@ -1,31 +0,0 @@
package org.kar.karideo.model;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.kar.archidata.annotation.SQLComment;
import org.kar.archidata.annotation.SQLIfNotExists;
import org.kar.archidata.annotation.SQLLimitSize;
import org.kar.archidata.annotation.SQLNotNull;
import org.kar.archidata.annotation.SQLTableName;
import org.kar.archidata.model.GenericTable;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@SQLTableName ("data")
@SQLIfNotExists
@JsonSerialize(include=JsonSerialize.Inclusion.NON_NULL)
public class Data extends GenericTable {
@SQLNotNull
@SQLLimitSize(128)
@SQLComment("Sha512 of the data")
public String sha512;
@SQLNotNull
@SQLLimitSize(128)
@SQLComment("Mime -type of the media")
public String mimeType;
@SQLNotNull
@SQLComment("Size in Byte of the data")
public Long size;
}

View File

@ -1,8 +0,0 @@
package org.kar.karideo.model;
public class DataGetToken {
public String login;
public String method;
public String time;
public String password;
}

View File

@ -168,7 +168,10 @@ export class HttpWrapperService {
server = environment.defaultServer;
}
const basePage = environment.server[server];
let addressServerRest = `${basePage }/`;
let addressServerRest = basePage;
if (!basePage.endsWith("/")) {
addressServerRest = `${basePage}/`;
}
let options = inputOptions;
if(isNullOrUndefined(options)) {
options = {};
@ -177,7 +180,11 @@ export class HttpWrapperService {
if (isArrayOfs(api, isString, isNumber, isBoolean)) {
for (let iii=0; iii<api.length; iii++) {
let elem = api[iii];
out += `/${elem}`;
if (out.endsWith("/")) {
out += elem;
} else {
out += `/${elem}`;
}
}
} else {
out += api;
@ -346,6 +353,7 @@ export class HttpWrapperService {
// Complex wrapper to simplify interaction:s
putSpecific(urlPath: UrlPath, data: object):Promise<ModelResponseHttp> {
console.log(`Put on ${urlPath}`);
return new Promise((resolve, reject) => {
this.request({
endPoint: urlPath,